kjcc Claude Sonnet 4.6 commited on
Commit
2ae5a57
Β·
1 Parent(s): c35c6cb

Initial commit: end-to-end MLOps pipeline project

Browse files

Includes Dagster orchestration, MLflow tracking, Evidently monitoring,
FastAPI serving, and Gradio frontend. Adds .gitignore for Python/MLOps
artifacts (venv, mlruns, model binaries, generated reports).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

.gitignore ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ *.whl
12
+
13
+ # Virtual environment (uv)
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Environment variables
19
+ .env
20
+ .env.*
21
+ !.env.example
22
+
23
+ # Jupyter
24
+ .ipynb_checkpoints/
25
+ *.ipynb~
26
+
27
+ # MLflow experiment tracking
28
+ mlruns/
29
+ mlartifacts/
30
+
31
+ # Trained model artifacts (track metadata/JSON, not binaries)
32
+ models/*.joblib
33
+ models/*.pkl
34
+ models/*.h5
35
+ models/*.pt
36
+ models/*.onnx
37
+
38
+ # Generated reports (Evidently)
39
+ reports/
40
+
41
+ # Dagster storage
42
+ storage/
43
+ $DAGSTER_HOME/
44
+ .dagster/
45
+
46
+ # Data files at root level only (duplicates of data/ directory)
47
+ /Computer_Durability.csv
48
+ /Computer_Durability_Plus.csv
49
+
50
+ # OS
51
+ .DS_Store
52
+ Thumbs.db
53
+
54
+ # IDE
55
+ .vscode/
56
+ .idea/
57
+ *.swp
58
+ *.swo
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.12
README.md CHANGED
@@ -1,3 +1,4 @@
 
1
  ---
2
  title: MLOps End2end MSDS2026
3
  emoji: πŸ†
@@ -11,3 +12,184 @@ pinned: false
11
  ---
12
 
13
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <<<<<<< HEAD
2
  ---
3
  title: MLOps End2end MSDS2026
4
  emoji: πŸ†
 
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
15
+ =======
16
+ # MLOps End-to-End Pipeline β€” Computer Durability Classifier
17
+
18
+ Predicts whether a computer **needs replacement** using a full MLOps stack:
19
+
20
+ | Component | Role |
21
+ |---|---|
22
+ | **Dagster** | Workflow orchestration (asset-based DAG) |
23
+ | **MLflow** | Experiment tracking + Model Registry |
24
+ | **Evidently** | Data drift detection + quality monitoring |
25
+ | **FastAPI** | REST prediction endpoint |
26
+ | **Gradio** | Interactive demo UI (HF Spaces–ready) |
27
+
28
+ ---
29
+
30
+ ## Dataset
31
+
32
+ | File | Rows | Description |
33
+ |---|---|---|
34
+ | `Computer_Durability.csv` | 999 | Original dataset |
35
+ | `Computer_Durability_Plus.csv` | 2,999 | Original + 2,000 synthetic rows with mild drift |
36
+
37
+ **Features:** Hours Used Per Day Β· Cost Β· User Age Β· Primary Usage Β· Brand Β· Computer Age (Months)
38
+ **Target:** `Needs Replacement` (binary, ~5–6% positive)
39
+
40
+ **Drift injected:** synthetic cohort has +2h/day average usage and βˆ’$3k average cost β€” detectable by Evidently's drift report.
41
+
42
+ ---
43
+
44
+ ## Setup
45
+
46
+ ```bash
47
+ # 1. Create venv with Python 3.12 and install all dependencies
48
+ uv venv --python 3.12
49
+ uv pip install -e ".[dev]"
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Running the Pipeline
55
+
56
+ ### Option A β€” Programmatic (script)
57
+
58
+ ```bash
59
+ uv run python scripts/run_pipeline.py
60
+ ```
61
+
62
+ Runs all 9 Dagster assets in order. Produces:
63
+ - `models/` β€” trained RF + XGBoost models, scaler, Optuna trial CSV
64
+ - `reports/` β€” Evidently HTML reports (data quality, drift, classification)
65
+ - `mlruns/` β€” MLflow experiment + model registry
66
+
67
+ ### Option B β€” Dagster Web UI
68
+
69
+ ```bash
70
+ uv run dagster dev
71
+ # Opens http://localhost:3000
72
+ # Navigate to Assets β†’ Materialize All
73
+ ```
74
+
75
+ ### Data synthesis only
76
+
77
+ ```bash
78
+ uv run python scripts/synthesize_data.py
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Model Serving
84
+
85
+ ### Start FastAPI
86
+
87
+ ```bash
88
+ uv run uvicorn serving.api:app --host 0.0.0.0 --port 8000 --reload
89
+ ```
90
+
91
+ Endpoints:
92
+ - `GET /health` β€” liveness check
93
+ - `GET /info` β€” model version + metrics
94
+ - `POST /predict` β€” single prediction
95
+ - `POST /predict/batch` β€” batch predictions
96
+
97
+ Example:
98
+ ```bash
99
+ curl -X POST http://localhost:8000/predict \
100
+ -H "Content-Type: application/json" \
101
+ -d '{
102
+ "hours_used_per_day": 20.0,
103
+ "cost": 12000,
104
+ "user_age": 50,
105
+ "primary_usage": 1,
106
+ "brand": 2,
107
+ "computer_age_months": 48
108
+ }'
109
+ ```
110
+
111
+ ### Start Gradio Demo
112
+
113
+ ```bash
114
+ # In a second terminal (API must be running on port 8000)
115
+ uv run python app.py
116
+ # Opens http://localhost:7860
117
+ ```
118
+
119
+ ### MLflow UI
120
+
121
+ ```bash
122
+ uv run mlflow ui --backend-store-uri mlruns/
123
+ # Opens http://localhost:5000
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Project Structure
129
+
130
+ ```
131
+ β”œβ”€β”€ Computer_Durability.csv # Original 999-row dataset
132
+ β”œβ”€β”€ Computer_Durability_Plus.csv # Augmented 2,999-row dataset (generated)
133
+ β”œβ”€β”€ app.py # Gradio frontend (HF Spaces–ready)
134
+ β”œβ”€β”€ pyproject.toml # uv/pip project + dependencies
135
+ β”œβ”€β”€ .python-version # Python 3.12
136
+ β”‚
137
+ β”œβ”€β”€ src/mlops_pipeline/
138
+ β”‚ β”œβ”€β”€ config.py # Paths, column names, MLflow settings
139
+ β”‚ β”œβ”€β”€ resources.py # Dagster MLflow resource
140
+ β”‚ β”œβ”€β”€ definitions.py # Dagster Definitions (asset wiring)
141
+ β”‚ └── assets/
142
+ β”‚ β”œβ”€β”€ data_assets.py # raw_data, augmented_data, train_test_split
143
+ β”‚ β”œβ”€β”€ training_assets.py # baseline_rf_model, tuned_xgb_model, best_model_info
144
+ β”‚ └── evaluation_assets.py # data_quality_report, data_drift_report, model_eval_report
145
+ β”‚
146
+ β”œβ”€β”€ serving/
147
+ β”‚ └── api.py # FastAPI prediction server
148
+ β”‚
149
+ β”œβ”€β”€ scripts/
150
+ β”‚ β”œβ”€β”€ synthesize_data.py # Generates Computer_Durability_Plus.csv
151
+ β”‚ └── run_pipeline.py # Runs all Dagster assets programmatically
152
+ β”‚
153
+ β”œβ”€β”€ models/ # Trained model artifacts (generated)
154
+ β”œβ”€β”€ reports/ # Evidently HTML reports (generated)
155
+ β”œβ”€β”€ mlruns/ # MLflow tracking store (generated)
156
+ └── data/raw/ # Raw CSV copies
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Pipeline Asset DAG
162
+
163
+ ```
164
+ raw_data ──────────────────────────────────────────┐
165
+ β”‚
166
+ augmented_data ──► train_test_split_asset ──► baseline_rf_model ──► best_model_info ──► model_eval_report
167
+ β”‚ β”‚
168
+ └──────────────────► tuned_xgb_model β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
169
+ β”‚
170
+ augmented_data ──────────────────────────────────────────────► data_quality_report
171
+ data_drift_report
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Results (actual run)
177
+
178
+ | Model | ROC-AUC | Avg Precision | F1 |
179
+ |---|---|---|---|
180
+ | RandomForest (baseline) | 0.827 | 0.246 | 0.314 |
181
+ | **XGBoost + Optuna (winner)** | **0.841** | **0.289** | **0.376** |
182
+
183
+ **Champion model:** `ComputerDurabilityClassifier v1 @champion` in MLflow Registry
184
+
185
+ High Avg Precision (~0.29) on a 6% positive-rate dataset is meaningful β€” random baseline would score 0.06.
186
+
187
+ ---
188
+
189
+ ## HF Spaces Deployment
190
+
191
+ The `app.py` at the project root is already structured for Hugging Face Spaces:
192
+ 1. Push the repo to HF
193
+ 2. Set `HF_API_URL` as a Space secret pointing to your deployed FastAPI instance
194
+ 3. Spaces will auto-launch `app.py`
195
+ >>>>>>> c1e8f69 (Initial commit: end-to-end MLOps pipeline project)
app.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio demo app β€” HF Spaces compatible (place this file at repo root).
3
+
4
+ In local mode it hits the FastAPI server at API_URL.
5
+ For HF Spaces, set the HF_API_URL secret or update API_URL to your deployed endpoint.
6
+
7
+ Start locally:
8
+ # Terminal 1: uv run uvicorn serving.api:app --port 8000
9
+ # Terminal 2: uv run python app.py
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+
15
+ import gradio as gr
16
+ import requests
17
+
18
+ API_URL = os.getenv("HF_API_URL", "http://localhost:8000")
19
+
20
+ PRIMARY_USAGE_MAP = {
21
+ "Work / Productivity (1)": 1,
22
+ "Gaming (2)": 2,
23
+ "Study / Education (3)": 3,
24
+ "Personal / Entertainment (4)": 4,
25
+ }
26
+ BRAND_MAP = {
27
+ "Brand A (1)": 1,
28
+ "Brand B (2)": 2,
29
+ "Brand C (3)": 3,
30
+ "Brand D (4)": 4,
31
+ "Brand E (5)": 5,
32
+ }
33
+
34
+
35
+ def predict(
36
+ hours_per_day: float,
37
+ cost: float,
38
+ user_age: float,
39
+ primary_usage: str,
40
+ brand: str,
41
+ computer_age_months: float,
42
+ ) -> tuple[str, str]:
43
+ payload = {
44
+ "hours_used_per_day": hours_per_day,
45
+ "cost": cost,
46
+ "user_age": user_age,
47
+ "primary_usage": PRIMARY_USAGE_MAP[primary_usage],
48
+ "brand": BRAND_MAP[brand],
49
+ "computer_age_months": computer_age_months,
50
+ }
51
+ try:
52
+ resp = requests.post(f"{API_URL}/predict", json=payload, timeout=10)
53
+ resp.raise_for_status()
54
+ data = resp.json()
55
+ except requests.exceptions.ConnectionError:
56
+ return (
57
+ "❌ Cannot connect to the prediction API.",
58
+ f"Make sure the FastAPI server is running at {API_URL}\n\n"
59
+ "Run: uv run uvicorn serving.api:app --port 8000",
60
+ )
61
+ except Exception as exc:
62
+ return "❌ Error", str(exc)
63
+
64
+ prob = data["probability"]
65
+ label = data["needs_replacement"]
66
+ version = data.get("model_version", "N/A")
67
+
68
+ verdict = "πŸ”΄ **Replacement Recommended**" if label else "🟒 **No Replacement Needed**"
69
+ details = (
70
+ f"**Replacement Probability:** {prob:.1%}\n\n"
71
+ f"**Decision Threshold:** 50%\n\n"
72
+ f"**Model:** {version}\n\n"
73
+ f"---\n"
74
+ f"*Inputs:* {hours_per_day:.1f}h/day Β· ${cost:,.0f} Β· "
75
+ f"{user_age:.0f}yo Β· {computer_age_months:.0f} months old"
76
+ )
77
+ return verdict, details
78
+
79
+
80
+ def build_demo() -> gr.Blocks:
81
+ with gr.Blocks(
82
+ title="Computer Durability Predictor",
83
+ theme=gr.themes.Soft(),
84
+ ) as demo:
85
+ gr.Markdown(
86
+ """
87
+ # πŸ’» Computer Durability Predictor
88
+ **MLOps End-to-End Pipeline** Β· Dagster Β· MLflow Β· Evidently Β· FastAPI Β· Gradio
89
+
90
+ Predict whether a computer **needs replacement** based on its usage profile.
91
+ The model was trained on the Computer Durability dataset using
92
+ **XGBoost + Optuna** hyperparameter tuning and tracked with **MLflow**.
93
+ """
94
+ )
95
+
96
+ with gr.Row():
97
+ with gr.Column(scale=1):
98
+ gr.Markdown("### Usage & Hardware")
99
+ hours = gr.Slider(
100
+ label="Hours Used Per Day",
101
+ minimum=1.0, maximum=24.0, step=0.5, value=10.0,
102
+ )
103
+ cost = gr.Slider(
104
+ label="Computer Cost (USD)",
105
+ minimum=5000, maximum=50000, step=500, value=30000,
106
+ )
107
+ comp_age = gr.Slider(
108
+ label="Computer Age (Months)",
109
+ minimum=1, maximum=60, step=1, value=24,
110
+ )
111
+
112
+ with gr.Column(scale=1):
113
+ gr.Markdown("### User Profile")
114
+ user_age = gr.Slider(
115
+ label="User Age (Years)",
116
+ minimum=8, maximum=65, step=1, value=35,
117
+ )
118
+ usage_type = gr.Dropdown(
119
+ label="Primary Usage",
120
+ choices=list(PRIMARY_USAGE_MAP.keys()),
121
+ value="Work / Productivity (1)",
122
+ )
123
+ brand = gr.Dropdown(
124
+ label="Brand",
125
+ choices=list(BRAND_MAP.keys()),
126
+ value="Brand C (3)",
127
+ )
128
+
129
+ predict_btn = gr.Button("Predict", variant="primary", size="lg")
130
+
131
+ with gr.Row():
132
+ verdict_out = gr.Markdown(label="Verdict")
133
+ details_out = gr.Markdown(label="Details")
134
+
135
+ predict_btn.click(
136
+ fn=predict,
137
+ inputs=[hours, cost, user_age, usage_type, brand, comp_age],
138
+ outputs=[verdict_out, details_out],
139
+ )
140
+
141
+ gr.Markdown(
142
+ """
143
+ ---
144
+ ### Architecture
145
+ ```
146
+ CSV Data β†’ Dagster Pipeline β†’ MLflow (tracking + registry)
147
+ β†’ Evidently (drift + quality reports)
148
+ β†’ FastAPI β†’ Gradio (this app)
149
+ ```
150
+ **Key signals driving replacement probability:**
151
+ - Hours/day > 12 β†’ risk increases significantly
152
+ - Cost < $20k β†’ cheaper machines fail more often
153
+ - Older users (>40) β†’ slight additional risk
154
+ """
155
+ )
156
+
157
+ return demo
158
+
159
+
160
+ if __name__ == "__main__":
161
+ demo = build_demo()
162
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
data/raw/Computer_Durability.csv ADDED
@@ -0,0 +1,1001 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ο»ΏHours Used Per Day,Cost,User Age,Needs Replacement,Primary Usage,Brand,Computer Age (Months)
2
+ 16.89428369,35266.8221,22.91722397,0,3,2,17.58335924
3
+ 7.247563169,45843.37058,22.07779155,0,3,3,40.4131477
4
+ 5.384026161,45981.07025,59.65651109,0,3,4,11.42425654
5
+ 5.246546519,20297.32086,22.22413339,0,4,5,35.2334834
6
+ 8.86847528,43341.38919,23.50113439,0,3,2,54.3380642
7
+ 10.88902525,33822.00801,51.28570096,0,1,4,19.89068888
8
+ 20.11183093,49665.21624,33.63517102,0,2,3,40.54498459
9
+ 3.75749515,34496.96228,52.27250175,0,1,2,12.08631174
10
+ 17.90329449,49606.87836,11.72587098,0,1,2,37.86292852
11
+ 13.22346303,42637.08084,35.79155804,0,4,4,20.85057076
12
+ 7.622742882,27458.14654,9.91597521,0,3,2,13.46128726
13
+ 12.33485044,36151.84938,11.5712326,0,2,5,19.47774099
14
+ 7.898667605,49764.06918,59.66693484,0,1,5,11.55162304
15
+ 14.70988497,45725.91823,15.93698615,0,3,4,54.21305036
16
+ 10.97841023,22921.00681,38.34797889,0,3,3,24.43402629
17
+ 4.781327169,41191.1599,31.43244935,0,4,4,15.47147899
18
+ 3.681410715,30367.38758,27.7985696,0,4,5,27.45606262
19
+ 13.57400069,20209.46786,59.2905007,0,4,1,19.91163384
20
+ 21.75549977,32104.42907,9.243933616,0,2,1,44.92073597
21
+ 8.911026791,27042.63265,45.83601211,0,1,2,58.3019748
22
+ 17.85900247,44566.07812,62.91348275,0,4,2,26.84398575
23
+ 16.14921116,30119.91827,39.92958646,0,4,1,51.8009706
24
+ 22.42576303,44825.71082,61.39888034,0,4,5,34.38592134
25
+ 19.8825634,20017.93817,10.97869912,0,1,2,10.27690424
26
+ 14.03192095,29234.5745,31.87121919,0,1,4,41.74674178
27
+ 16.11170146,27234.95643,22.82899408,0,4,4,31.29263533
28
+ 21.66288873,27007.02083,49.656795,0,4,5,39.38578487
29
+ 10.18893205,22045.59859,63.93393416,0,3,2,53.49155734
30
+ 8.516278235,25613.72811,22.62221356,0,4,4,26.98563967
31
+ 1.248654924,36666.4871,45.28795228,0,2,2,54.16947977
32
+ 20.01067471,28740.83219,19.29156507,0,2,2,45.28947184
33
+ 19.42255611,32521.84347,40.22382451,0,4,4,29.82769063
34
+ 3.402453916,31306.00641,34.44415174,0,1,1,36.590106
35
+ 14.26771977,46277.14775,63.40430379,0,3,4,20.20928682
36
+ 11.67157906,47158.96997,42.68605454,0,1,5,59.03224068
37
+ 3.730309197,34788.70915,27.92186321,0,4,3,40.37371862
38
+ 23.55306922,29904.67851,14.50345974,0,2,4,3.586964297
39
+ 5.938005412,23204.02112,16.62106924,0,3,2,20.27069244
40
+ 2.546386142,48602.08638,20.84306501,0,1,2,37.77555159
41
+ 14.68148973,21806.80835,22.3050997,0,1,4,4.32366339
42
+ 17.99393357,28929.50833,56.4851153,0,4,3,2.409167288
43
+ 1.746199259,28941.98604,39.98969893,0,3,4,27.55143013
44
+ 16.15050569,29612.03858,37.83328195,0,2,3,20.6287206
45
+ 13.24271315,24928.6053,14.5418257,0,4,3,26.85961787
46
+ 3.375095204,43494.84293,57.02796252,0,3,5,16.49890528
47
+ 4.96804917,23674.93586,49.20041528,0,1,2,10.71834707
48
+ 14.06340884,39047.19977,11.85709663,0,4,2,41.65945806
49
+ 7.816225111,23715.3808,48.34660054,0,1,3,31.35969586
50
+ 22.58126176,22116.06913,38.98167839,0,1,2,12.80909846
51
+ 19.83102528,47282.88511,12.65834471,0,4,1,19.85577615
52
+ 23.62204575,25703.86299,34.12313657,0,4,3,35.91666655
53
+ 6.972429296,44637.50464,35.62768837,0,4,2,5.261531548
54
+ 23.30364355,36338.92613,17.44914879,0,4,1,9.699727547
55
+ 10.91837901,22335.69559,61.90479438,0,4,5,5.321490642
56
+ 22.17731082,16907.3274,56.44859617,1,1,2,48.92672263
57
+ 8.992932292,48663.44574,46.13427323,0,2,1,45.39214476
58
+ 2.15803912,27408.83501,34.35084698,0,2,4,45.44546298
59
+ 2.213600931,45789.36036,31.47063587,0,4,4,43.26319527
60
+ 16.91625889,25064.30017,45.10548763,0,1,1,40.49374
61
+ 11.53236687,45618.28646,39.08961656,0,2,3,29.14252301
62
+ 6.209997399,20396.86439,11.54956704,0,2,3,38.89441165
63
+ 15.12131705,35353.4137,37.21265098,0,2,2,3.263331732
64
+ 6.81573672,43083.27738,53.96500618,0,1,4,37.66640094
65
+ 14.28553035,48043.86552,34.17667302,0,4,3,46.89478463
66
+ 9.261801181,47276.06409,10.96152492,0,4,3,30.52114326
67
+ 19.94170121,44588.74609,52.81785736,0,4,4,22.09328638
68
+ 19.8760117,46645.74049,19.47773558,0,3,5,17.36817735
69
+ 11.97905916,25976.59058,22.74138759,0,3,3,38.92532096
70
+ 9.068839515,28525.52887,17.38826215,0,3,1,41.87527072
71
+ 9.338875321,28669.05575,26.8222587,0,1,2,52.38708839
72
+ 19.53143409,31311.61793,51.13483582,0,2,3,21.19771087
73
+ 8.548128104,31799.62011,37.60499399,0,2,3,46.41656614
74
+ 5.808365595,36336.65974,19.67823263,0,1,5,34.11999363
75
+ 2.947831863,24796.3929,58.03631445,0,1,1,53.08735295
76
+ 23.36593755,13302.91308,58.13616573,1,4,2,32.14091439
77
+ 11.73394178,40737.39991,57.62297023,0,4,3,31.54015988
78
+ 12.08903828,25026.99618,21.61138405,0,3,2,35.09474781
79
+ 20.34090034,29309.25446,33.72064262,0,3,4,45.20960595
80
+ 6.088490018,35100.35392,64.1444104,0,1,3,57.91673825
81
+ 9.762799294,43893.21685,52.00471115,0,3,3,5.340237899
82
+ 19.59333199,42184.077,9.54854301,0,2,4,4.152641541
83
+ 19.94334305,39366.03069,11.7166617,0,1,5,56.31546991
84
+ 9.846054748,24673.19782,34.44408848,0,1,3,32.26266191
85
+ 11.55985254,36411.36037,59.82555173,0,2,2,37.27927786
86
+ 7.97557549,22623.39505,38.70600248,0,4,1,29.46299249
87
+ 22.4487622,31320.8953,36.37531296,0,4,2,6.525789704
88
+ 3.728806612,32405.49591,14.0120009,0,2,5,46.31727333
89
+ 22.61826141,13443.84468,45.43646597,1,2,1,45.08625406
90
+ 22.47623673,21194.34318,54.85988015,0,3,2,19.39049597
91
+ 16.72892839,28219.64298,29.68394043,0,1,2,19.06811547
92
+ 20.86880371,20701.27842,52.2098755,1,3,4,50.43264147
93
+ 13.22615951,48669.48402,62.97516927,0,2,5,5.269078207
94
+ 1.751158398,48608.97823,19.61468817,0,3,4,52.98520375
95
+ 11.93871241,30569.81163,37.82978898,0,2,1,16.1430397
96
+ 15.32957616,8914.252802,24.36686361,1,3,4,35.98610012
97
+ 10.37820642,21293.00327,53.19270188,0,3,5,56.22365656
98
+ 4.708032675,25372.84818,40.92282186,0,4,3,41.00922759
99
+ 16.09742088,31766.5338,44.1711978,0,2,3,25.45135894
100
+ 23.33647087,48433.39094,53.4811071,0,3,1,41.68251833
101
+ 13.93034265,24235.97191,30.57031694,0,1,3,21.29479854
102
+ 17.43932536,42905.47531,60.1601339,0,3,4,17.35824656
103
+ 2.574360409,23260.47618,38.38264543,0,3,1,59.8043134
104
+ 10.6063409,49437.72732,17.0034249,0,1,5,45.29662364
105
+ 3.676245159,31448.49497,47.66624974,0,2,1,39.03847867
106
+ 15.09430422,43257.03617,53.21589697,0,4,1,21.96208895
107
+ 22.58508964,30277.73248,26.05541559,0,4,2,3.90048892
108
+ 16.22088785,38830.06631,56.85921765,0,4,2,2.832355686
109
+ 2.766470111,25760.48254,59.65016552,0,1,1,33.69669407
110
+ 9.157322715,24380.63058,23.78355578,0,3,3,45.95993054
111
+ 13.66525773,48141.92419,64.06072394,0,2,2,32.70537224
112
+ 10.27377839,47529.41834,16.0205571,0,3,5,50.60631924
113
+ 20.17381224,46624.06821,19.5148931,0,4,4,2.972165453
114
+ 19.73743311,38763.0035,18.50081584,0,3,2,22.63837251
115
+ 15.07211346,44248.04634,58.95741347,0,1,1,14.98715308
116
+ 9.589532738,22071.10811,45.29467548,0,1,3,27.38910512
117
+ 6.872736597,30043.67126,16.66994414,0,2,4,5.270207198
118
+ 3.435109319,29411.09374,33.09843485,0,2,2,57.29376685
119
+ 9.16264672,39069.35966,43.07198743,0,4,3,14.17206866
120
+ 10.5029679,30678.7332,12.75745284,0,2,3,55.41615875
121
+ 16.54392728,24455.20345,58.297733,0,3,3,26.12769615
122
+ 16.13767596,37894.01226,53.80540152,0,1,1,59.14592386
123
+ 2.612487212,44431.51762,36.79678609,0,1,5,33.68944462
124
+ 10.09096425,31463.70483,63.12621707,0,1,3,55.91913899
125
+ 5.180913946,23968.06145,31.81237582,0,2,4,1.315590333
126
+ 4.606692222,25900.64592,64.09428819,0,1,2,45.14608956
127
+ 20.02794944,27124.49916,46.07143993,0,1,4,54.27998853
128
+ 1.964049984,28996.92779,44.17626258,0,1,1,9.243513452
129
+ 10.64120615,39283.85212,17.45942903,0,4,1,6.671425252
130
+ 4.868684083,38028.36454,58.26988222,0,4,4,26.04978382
131
+ 21.22459496,40882.39585,32.36691221,0,2,5,56.08705546
132
+ 13.81193181,30875.05954,17.24730413,0,1,1,33.00080705
133
+ 6.321754637,44719.92956,8.718628639,0,2,1,32.11993136
134
+ 12.5558276,26122.62983,39.90607397,0,1,4,9.32644098
135
+ 17.82418627,34071.22243,38.06177846,0,1,1,1.046581154
136
+ 23.03671421,44028.16165,49.00315648,0,1,5,48.79826535
137
+ 6.070258098,25404.75943,58.74470902,0,2,5,6.166899216
138
+ 21.38218076,23295.89341,12.52617532,0,2,5,38.39713931
139
+ 22.48899463,46510.51903,49.69529995,0,1,2,54.96237082
140
+ 22.07767252,30287.88988,18.68248081,0,3,5,13.31046288
141
+ 15.61453021,40516.10375,56.91609203,0,2,5,40.25109734
142
+ 15.48587505,20760.45813,54.68662029,0,4,2,5.643216026
143
+ 10.27384456,40701.40665,38.82524403,0,2,3,36.62155349
144
+ 18.30515129,29483.03935,48.48383952,0,1,1,10.67197154
145
+ 13.21976242,21672.22561,25.91795645,0,3,5,44.42126007
146
+ 16.58107564,38294.38285,34.85657024,0,1,1,18.77264639
147
+ 10.85453179,27224.11448,54.83330317,0,4,4,28.34006952
148
+ 17.82441867,31604.21726,34.17811624,0,2,1,36.36640198
149
+ 19.9628449,22417.42437,28.39450112,0,2,2,11.10513123
150
+ 4.368033902,24414.89753,36.17010949,0,1,1,44.7566313
151
+ 20.15738346,28939.31706,55.21020739,0,1,1,18.8557801
152
+ 13.42682023,22692.78502,27.1068587,0,4,1,40.07993973
153
+ 20.42731591,47324.78794,17.90441081,0,3,3,10.33869999
154
+ 10.91993925,20181.4113,48.58479916,0,4,3,41.59894189
155
+ 9.717931164,34515.80657,55.08075812,0,3,4,8.563096296
156
+ 22.04639685,23697.42374,13.73633628,0,2,2,26.09603328
157
+ 6.776893154,29230.37268,21.67282443,0,3,1,15.58515835
158
+ 20.41688348,47781.78679,16.09240342,0,3,1,45.15413972
159
+ 12.13247317,24850.8366,27.83266046,0,2,1,16.46006527
160
+ 12.82419518,48974.96959,33.6700058,0,2,1,53.02132108
161
+ 8.094770474,26173.83131,50.68310121,0,2,3,53.61739247
162
+ 14.17997061,42246.98325,45.11541234,0,4,4,12.5900376
163
+ 8.479857514,36400.31795,43.39292835,0,2,5,37.21691076
164
+ 15.37939251,21405.06638,28.08537196,1,1,2,6.175340861
165
+ 1.903196528,46534.36183,55.96251053,0,2,1,24.10068888
166
+ 7.255967923,36619.79767,34.86336854,0,2,4,43.08598204
167
+ 1.146717998,36036.77657,63.80898147,0,2,5,39.90597156
168
+ 23.5021601,25863.74001,44.14610188,0,3,4,37.85733949
169
+ 23.21218583,24986.88086,15.19709027,0,4,3,17.66991096
170
+ 10.08857184,47494.67453,46.54212104,0,2,5,23.30347202
171
+ 17.75252965,29835.14597,26.53092888,0,4,4,7.073822537
172
+ 8.953540263,31162.46765,47.12065021,0,1,3,43.42234641
173
+ 16.43514261,42794.70975,11.96954545,0,1,4,30.72943901
174
+ 19.52489099,30164.70447,17.96824192,0,2,4,42.81717231
175
+ 22.77539363,48561.8734,56.77702376,0,1,5,15.68766796
176
+ 10.20488234,42137.83307,20.94923475,0,4,3,20.14185549
177
+ 19.01585361,37922.87728,55.71134414,0,3,1,20.7187049
178
+ 23.56032385,17974.94228,23.91873704,1,2,2,2.518802175
179
+ 7.116354365,46426.85482,44.64426887,0,1,5,21.1218245
180
+ 23.77764299,21488.17725,47.56658269,0,4,3,48.39496984
181
+ 1.592700772,27711.88077,37.22132093,0,4,4,53.94061125
182
+ 14.88116255,33951.09003,25.40270857,0,1,3,31.57488062
183
+ 16.16863758,26809.35618,20.12072945,0,4,2,27.87460002
184
+ 16.82504781,42317.15562,9.89179321,0,4,2,2.818652503
185
+ 3.768914312,47576.24093,25.32494487,0,2,1,52.87305592
186
+ 22.58618502,32713.1036,45.23027906,0,4,1,1.767602592
187
+ 5.167215418,30263.978,61.48337465,0,1,2,56.74025473
188
+ 15.32332827,48846.52196,57.65865549,0,2,5,29.86375867
189
+ 6.12154982,33345.57298,51.66569226,0,1,5,21.17413512
190
+ 8.065977591,34671.45201,52.94149857,0,3,4,55.52888766
191
+ 12.12341395,13379.39548,45.9041395,1,1,1,2.439068593
192
+ 13.56854646,30549.49617,22.83635088,0,1,4,35.3040688
193
+ 10.59544863,47286.45419,59.71011675,0,2,1,33.38701895
194
+ 4.685188822,37068.6645,46.23174178,0,4,2,13.93990367
195
+ 4.931537256,42865.23762,39.94511597,0,1,5,26.30435845
196
+ 10.61733013,44354.20155,14.32639975,0,2,5,39.48357635
197
+ 20.59259968,14160.57233,33.482166,1,3,2,43.90074062
198
+ 18.4175085,30255.47126,34.24042146,0,2,4,1.042294664
199
+ 21.65318688,33010.82994,57.28012574,0,2,4,56.27435238
200
+ 2.934936232,45454.15323,39.15894855,0,4,3,10.70699697
201
+ 10.04138987,30897.66055,29.68283129,0,1,1,14.78124562
202
+ 3.305647966,43818.054,63.67761873,0,3,4,16.5449333
203
+ 1.381627055,48229.76968,14.31107382,0,2,2,3.231388057
204
+ 16.2195297,38696.31119,32.08526041,0,1,4,8.260075292
205
+ 14.85633717,42248.81862,10.39540648,0,1,5,59.19464545
206
+ 4.749439618,44152.91936,50.17453488,0,2,1,19.59778751
207
+ 20.47495395,10132.94414,60.33036348,1,1,3,7.80276433
208
+ 6.377695161,45351.18947,23.96212778,0,2,1,7.312484409
209
+ 1.545278334,31851.60014,56.92547558,0,2,5,43.44813809
210
+ 20.20378573,21537.90378,24.65643914,0,2,5,51.41955118
211
+ 19.47227449,20042.28665,59.91389921,1,3,5,16.16018567
212
+ 23.41788371,32056.28814,50.97582851,0,2,5,2.233661228
213
+ 4.115569346,40212.68689,53.87968628,0,4,2,14.16231109
214
+ 6.323929917,33510.51157,9.026373669,0,1,3,4.334425757
215
+ 20.97341905,42468.89453,62.88019035,0,3,5,7.100301261
216
+ 22.30866735,30709.47288,49.42024655,0,1,5,26.45897907
217
+ 10.6514298,39042.32423,25.37081244,0,3,4,14.56518388
218
+ 2.164316862,25592.59415,55.2754959,0,4,3,31.44080683
219
+ 1.906012175,42481.0831,24.04670831,0,3,1,15.95613144
220
+ 14.20084692,36370.87906,57.74696886,0,3,3,56.49170077
221
+ 10.0365875,26121.93166,14.41708225,0,3,3,54.45069411
222
+ 1.659005834,28757.3352,48.11010072,0,1,3,34.4415602
223
+ 14.41480502,42458.26126,38.81981317,0,2,5,21.91101553
224
+ 1.259958342,34478.03707,13.5024648,0,1,5,23.48766604
225
+ 19.10955504,26480.94321,21.78794821,0,2,2,25.29928471
226
+ 8.047954922,49018.13571,8.707030068,0,1,1,43.92718001
227
+ 1.925934069,41453.75059,34.71977403,0,4,4,27.64670074
228
+ 14.53210108,48714.04498,25.17211986,0,4,3,51.54706684
229
+ 10.14674153,32813.15136,42.1063766,0,3,1,56.70159237
230
+ 23.39740951,34253.01022,24.94255504,0,4,1,20.82082305
231
+ 13.51656924,46938.65649,25.09538936,0,3,2,34.0809744
232
+ 7.330501164,35751.48021,50.36201233,0,2,4,6.608335673
233
+ 17.31709389,41218.36736,10.74414165,0,4,3,12.82767382
234
+ 23.88130615,15257.49485,59.46501473,1,2,4,25.39448024
235
+ 7.244316058,27565.18632,56.57904084,0,1,2,13.16596024
236
+ 21.7975957,33401.79406,46.06486694,0,2,5,52.71706896
237
+ 9.618605322,23703.49597,41.8136295,0,1,1,10.33888911
238
+ 15.74255323,11417.09085,58.86124239,1,2,3,27.88398847
239
+ 13.64697694,26179.40108,18.56382023,0,2,5,36.73625519
240
+ 2.163181388,20028.30246,12.50124224,0,4,2,41.81997058
241
+ 10.80078847,21177.95155,21.65207968,0,2,4,23.60109281
242
+ 20.14436141,49318.5147,53.29096189,0,4,1,52.27684505
243
+ 19.52992252,27270.61085,9.976205203,0,4,3,34.53082991
244
+ 6.159326854,39899.32129,41.2199896,0,3,5,26.93095905
245
+ 6.199818633,45172.99096,64.73993842,0,3,5,4.967197409
246
+ 16.07966237,16921.00232,56.77467735,1,1,1,28.41667864
247
+ 19.79802218,36519.81378,37.72240428,0,2,4,49.42341461
248
+ 22.39916366,24594.335,11.62753183,0,1,2,37.25792008
249
+ 3.193301189,41843.49312,55.38829032,0,2,4,50.09758477
250
+ 11.35134871,38007.5423,42.14177497,0,3,4,30.14668857
251
+ 8.761448416,41937.51768,14.55118083,0,3,4,7.989240601
252
+ 21.02583448,43092.39869,13.34986523,0,2,1,1.804750904
253
+ 2.921073038,49262.31103,59.84872871,0,2,1,18.88995326
254
+ 5.855896679,37206.8984,46.14441499,0,3,2,42.18143782
255
+ 18.30677176,30249.23338,55.26934756,0,3,4,47.62533433
256
+ 2.179758527,39453.93671,58.10179732,0,1,1,18.44941724
257
+ 12.94727284,24914.68904,40.59102407,1,3,2,30.24358133
258
+ 12.32831376,22053.22128,37.49444205,0,2,2,25.84601536
259
+ 11.1684618,46921.91955,32.53436239,0,1,4,25.22084428
260
+ 8.691227117,23581.43034,26.0659555,0,2,3,14.64957408
261
+ 10.07516325,29835.28514,32.77196977,0,4,4,9.045895197
262
+ 13.1886335,44472.36142,52.11114038,0,1,3,12.60462494
263
+ 4.711449243,37919.37135,42.30963557,0,2,2,4.264610843
264
+ 14.1559052,31809.06561,58.87382753,0,3,1,12.68971377
265
+ 19.52494357,34203.06978,33.27266082,0,4,5,1.54285296
266
+ 18.48370139,45636.4218,42.60410599,0,2,2,38.00824564
267
+ 4.539697808,30200.13158,43.98453053,0,1,1,19.66614904
268
+ 4.432737804,46089.49054,41.72673825,0,2,2,58.41553797
269
+ 7.168010458,22644.03293,48.05012507,0,4,4,12.429306
270
+ 9.304718706,43303.95312,21.5337077,0,1,3,21.40388565
271
+ 10.39447837,45426.42899,37.20473532,0,4,2,22.11262836
272
+ 16.63303599,25454.52959,13.94081376,0,2,1,1.498638268
273
+ 2.30364994,32910.39597,29.91714568,0,3,4,50.51326923
274
+ 1.797472197,24964.99324,35.79702291,0,3,5,2.887673149
275
+ 10.01394295,41198.12387,45.17678279,0,2,4,56.47061025
276
+ 17.03476492,36060.43304,62.18027,0,2,3,53.63238671
277
+ 5.449012888,39059.7004,42.23711079,0,2,4,32.59579305
278
+ 15.75460307,25894.60901,50.38485322,0,1,5,33.07213892
279
+ 6.976047173,26352.37586,36.85716539,0,4,1,39.57289745
280
+ 21.3799805,21244.91977,44.14392979,0,4,2,23.8359507
281
+ 21.60086872,29662.02606,12.04313632,0,4,3,39.18611472
282
+ 7.837605698,36792.33984,22.50032011,0,2,5,3.641334933
283
+ 6.289856341,45738.04742,28.62562631,0,4,3,4.188810014
284
+ 10.45999136,40007.82342,34.93212124,0,2,4,27.97659476
285
+ 6.532226459,33061.8897,10.60197849,0,1,3,33.42771577
286
+ 16.4648284,48593.60343,15.98137356,0,3,5,53.38124078
287
+ 19.99948774,41577.35602,23.77841554,0,2,2,7.174977401
288
+ 16.48111901,47904.95609,63.37736338,0,2,2,49.5362277
289
+ 17.31357764,13705.24559,26.8867794,1,3,3,13.3927929
290
+ 19.96006029,35827.6392,35.47634031,0,3,1,24.25093526
291
+ 15.13258231,23413.82802,19.17756963,1,1,3,19.67954365
292
+ 10.13081973,27767.12824,42.81446405,0,4,4,24.6091611
293
+ 4.595290326,21584.80059,23.99894328,0,2,2,9.879234386
294
+ 17.97287179,41782.54554,19.79857697,0,4,2,30.71063439
295
+ 9.290913618,23639.08994,37.44463982,0,2,2,48.2969128
296
+ 16.43922898,29083.25385,8.313830552,0,1,1,27.05050661
297
+ 7.224810666,35974.12724,8.436849861,0,3,1,29.94620098
298
+ 2.868281601,36932.49763,20.48692212,0,1,5,51.07728753
299
+ 23.8293813,38017.49591,10.09311766,0,3,3,42.65742876
300
+ 4.592634682,24987.65991,14.15746799,0,4,3,29.9368101
301
+ 23.73368286,31392.12214,27.31505701,0,4,4,16.20404324
302
+ 23.47743845,38515.82681,53.74738374,0,1,2,53.21067806
303
+ 19.25781639,49094.56594,40.60677267,0,3,1,35.0442037
304
+ 16.16672813,41828.93413,37.22206038,0,1,4,14.047709
305
+ 14.28956216,47678.11271,24.72886292,0,3,4,16.87806541
306
+ 20.92033554,42866.36175,61.10996201,0,3,1,48.38485225
307
+ 7.657108997,37751.50907,30.62985735,0,4,3,3.604588163
308
+ 11.75666789,25760.70107,12.96428771,0,4,2,59.53343903
309
+ 15.2459693,40000.56956,43.17279142,0,3,3,8.690749103
310
+ 10.45738094,38701.38698,14.48878843,0,3,4,47.968043
311
+ 10.83218844,38074.07511,27.67771914,0,3,2,24.29174369
312
+ 8.596547353,34693.57604,36.92248051,0,4,5,30.04075481
313
+ 13.97733178,35859.69812,57.83068391,0,4,4,52.95838407
314
+ 20.56321414,30033.27581,36.13215508,0,2,3,25.36176019
315
+ 5.635152694,35579.38078,48.02874966,0,3,3,32.71213147
316
+ 22.49195989,25926.67717,64.59055982,0,2,3,3.908680082
317
+ 16.84901604,44155.23192,15.49487983,0,3,1,1.138500958
318
+ 19.93528389,25573.30864,23.65969695,0,4,4,15.44319251
319
+ 13.79238593,22548.9887,30.49083631,0,2,2,41.15440248
320
+ 18.92888413,33087.38079,32.04420618,0,2,3,8.154628708
321
+ 1.372609139,39749.22319,31.42820129,0,1,3,7.711955569
322
+ 19.82274977,33131.50475,59.73382715,0,3,1,25.24667587
323
+ 1.923193759,28302.62396,48.69986073,0,4,2,39.22238528
324
+ 21.46800766,36745.13552,42.6505972,0,1,4,43.01224754
325
+ 23.81514319,30410.07006,25.63424492,0,1,5,11.52858977
326
+ 7.76355163,47986.0307,54.9560519,0,3,1,5.695700695
327
+ 5.837326795,47679.35419,62.43844915,0,1,4,56.483425
328
+ 18.60335737,35071.1546,54.80830211,0,3,5,16.37931657
329
+ 16.12064043,11900.3368,8.089210996,1,1,3,51.34744336
330
+ 6.819605841,29836.51982,44.2748781,0,3,3,51.94599558
331
+ 20.90793481,42113.97242,10.91471674,0,4,2,46.57883291
332
+ 3.36537962,21101.3883,22.68361156,0,3,5,10.96118279
333
+ 3.896969517,34243.07018,11.39290792,0,3,3,49.4862861
334
+ 23.52048577,30089.70183,42.41634082,0,2,3,46.385783
335
+ 16.49829466,47616.07655,47.13562507,0,4,2,36.20665642
336
+ 20.47840616,20360.09082,14.52579537,0,4,4,20.89492007
337
+ 8.459575679,36584.44627,29.87881633,0,4,3,16.48800765
338
+ 16.55912624,42219.90743,34.00548101,0,1,1,8.191463513
339
+ 14.66756203,34550.53963,29.03602224,0,2,1,31.07566695
340
+ 14.87245276,22562.0909,14.89842503,0,4,4,35.53039166
341
+ 16.69824657,49173.8417,31.88002404,0,1,5,22.20100044
342
+ 14.23326098,35540.31294,50.81714873,0,1,2,29.04618499
343
+ 10.86843449,38425.58733,12.04743688,0,1,1,22.44735298
344
+ 7.346221971,27098.29348,12.5702608,0,2,1,38.81935738
345
+ 18.67737061,34504.94255,28.22196363,0,1,4,54.17429326
346
+ 6.20426589,32874.48476,61.67836556,0,2,5,10.67129546
347
+ 16.92414904,22246.87526,46.10864086,0,3,5,23.50311978
348
+ 6.366654921,23184.81061,46.68418753,0,1,4,9.311247633
349
+ 15.38259391,45124.17559,28.62943948,0,4,1,37.87168245
350
+ 18.18280014,27194.59857,41.8386651,0,1,2,44.00336006
351
+ 6.030419445,25848.74814,8.57747749,0,3,1,55.98285122
352
+ 2.37869592,35147.34354,44.25747465,0,3,5,17.45344365
353
+ 4.012396089,43062.41394,60.05735601,0,1,1,57.06246115
354
+ 14.93793025,21858.39556,42.9166877,0,3,2,4.732497637
355
+ 20.53726734,37304.23759,57.80081983,0,2,3,36.57781794
356
+ 2.034926888,23576.31329,49.26646416,0,2,1,4.370830441
357
+ 17.88547896,21093.83152,14.87182974,0,1,4,53.73267708
358
+ 8.850328857,21582.31856,59.43983852,0,2,3,32.25873437
359
+ 12.00615251,45024.92325,11.78729913,0,2,4,40.4915527
360
+ 22.36376965,23539.17451,38.43614594,0,4,4,50.06286163
361
+ 8.635344609,21354.54093,16.10181606,0,1,4,9.868326727
362
+ 11.70274948,33136.99661,8.667276812,0,3,1,3.276902154
363
+ 1.314695539,45319.80416,32.05595283,0,1,5,52.85398761
364
+ 2.87677056,27872.25129,24.81732034,0,2,3,19.58106042
365
+ 6.947606596,32651.37853,35.70202815,0,4,2,8.609755568
366
+ 1.640601539,21197.67598,40.90039354,0,1,5,28.63921299
367
+ 15.5216325,33472.35542,10.49312712,0,1,1,36.30005847
368
+ 10.80716221,37328.39242,15.01118782,0,2,4,2.827265602
369
+ 13.60733966,37127.21161,39.8426166,0,2,2,17.21884384
370
+ 5.016885276,29972.80077,27.56053913,0,1,3,2.935192582
371
+ 7.806436677,29465.99542,49.56288879,0,2,5,25.79978796
372
+ 16.26796292,23198.70857,45.18062397,0,2,5,53.81090171
373
+ 23.20196264,31003.94962,56.19944869,0,2,2,58.97649228
374
+ 2.159585793,22984.84007,47.47209112,0,2,1,38.58580566
375
+ 21.47883938,42997.87834,32.50607141,0,2,3,10.53662393
376
+ 14.26826763,48988.22893,46.35909748,0,4,2,54.44511401
377
+ 13.97015004,49102.2337,23.69672693,0,2,4,31.45862157
378
+ 12.50640631,45952.13298,25.45986943,0,3,3,10.07120941
379
+ 2.596063976,38023.76448,52.97215412,0,1,5,55.3818735
380
+ 3.06716759,41025.97092,33.44591243,0,4,2,45.69067399
381
+ 14.82097613,28556.17408,53.50767941,0,4,5,44.17061109
382
+ 8.842047143,38923.11261,54.8780753,0,2,5,42.84166152
383
+ 22.09716553,33669.48648,56.88105325,0,1,5,12.82004944
384
+ 10.35158236,34460.52019,60.24820844,0,3,1,13.44939481
385
+ 4.29547355,42733.29681,32.56615757,0,2,5,41.60928691
386
+ 17.43853317,28457.55067,26.17540864,0,1,4,13.96364226
387
+ 7.747128693,29541.18835,41.18532856,0,2,4,26.88163011
388
+ 13.08581707,47739.25373,29.15682814,0,2,3,47.13810771
389
+ 17.05015407,21686.72885,42.26122751,0,2,5,47.25319857
390
+ 21.70607456,34569.02895,48.21839417,0,2,5,27.74244458
391
+ 19.22039748,47384.02401,47.23908605,0,2,2,47.73920636
392
+ 22.01486148,6094.654138,29.34951456,1,2,1,50.47868598
393
+ 16.55630456,38310.89023,17.5110315,0,2,3,39.63176933
394
+ 16.6315632,36392.21223,32.54017229,0,3,5,14.47712529
395
+ 22.75626681,27492.32538,16.12786621,0,4,2,17.16669504
396
+ 7.803954242,30298.26467,58.73553101,0,3,1,19.57562817
397
+ 1.025578167,45561.45247,27.71501526,0,2,3,7.891442356
398
+ 7.254954866,36776.54395,16.80420559,0,2,3,17.1164176
399
+ 6.011501731,35442.26581,9.450522315,0,3,5,13.36163423
400
+ 16.22219546,22987.39141,44.81187602,0,3,3,27.86439624
401
+ 15.57536679,20480.84469,44.30343209,0,4,2,54.59781532
402
+ 14.6496118,48910.55566,27.4144375,0,3,3,7.164338688
403
+ 1.373599194,31309.58502,12.08757766,0,4,4,43.49580751
404
+ 17.76189525,38096.39186,31.34884701,0,2,2,41.54969726
405
+ 8.441199487,22391.4522,25.73939722,0,1,1,6.649497721
406
+ 16.29628775,40493.42857,46.59587646,0,1,1,55.43177189
407
+ 13.80130386,47977.00126,42.52941722,0,1,5,34.53985993
408
+ 8.882811285,37269.95759,28.78184251,0,1,5,22.45980575
409
+ 4.095254143,23783.45967,20.41987559,0,2,4,45.63577643
410
+ 3.171634646,37451.64582,64.31805007,0,1,1,16.18456205
411
+ 20.12492166,42990.41639,33.87809241,0,3,1,41.91708039
412
+ 22.12314347,32192.4961,47.23163144,0,3,1,3.342961247
413
+ 15.94512508,46395.13244,16.01151915,0,1,1,50.52507422
414
+ 3.378984642,39460.4886,35.67861288,0,3,3,27.44207997
415
+ 10.24662599,46977.46755,9.569593033,0,4,2,38.56488195
416
+ 23.22581416,9084.842127,36.81094172,1,2,2,14.05303137
417
+ 17.7656016,38705.15934,62.94899221,0,1,4,40.94734905
418
+ 18.93588033,23938.63916,29.89978264,0,1,3,46.56217628
419
+ 3.719028313,29290.75133,10.22277272,0,2,1,28.91957803
420
+ 1.000706533,26053.84845,9.764480082,0,3,1,37.44588852
421
+ 17.37915206,47264.79493,30.11524532,0,2,5,27.92322951
422
+ 9.201712048,42812.00202,17.12142138,0,2,5,53.75139079
423
+ 6.853070627,40291.95107,9.331069491,0,3,4,30.37428165
424
+ 1.296641805,29028.98959,51.104176,0,2,1,50.66671383
425
+ 13.42688636,25521.34316,34.13581151,0,3,4,48.27977144
426
+ 20.57646691,42693.53264,24.486952,0,3,3,25.21978446
427
+ 23.02552786,34224.73838,59.30474143,0,2,2,14.57498502
428
+ 14.22879686,13373.90287,14.61992073,1,4,1,24.95884582
429
+ 14.01213386,26773.68656,62.48737325,0,3,3,49.74682517
430
+ 12.83261054,38498.65934,25.89610289,0,2,1,14.43031961
431
+ 2.956296866,21210.01556,58.63900932,0,3,5,31.94174441
432
+ 13.62407444,29785.10762,42.36774396,0,4,4,48.55970813
433
+ 9.739518816,34056.70984,55.12231951,0,4,5,21.85517103
434
+ 14.96411146,24438.75002,64.0887503,0,1,2,38.89036858
435
+ 9.940706001,49539.63433,24.43910996,0,3,5,15.49226287
436
+ 6.526879839,26272.92094,62.78472267,0,3,1,7.447487277
437
+ 3.18420133,23910.5939,30.20046801,0,2,5,47.86866928
438
+ 8.243124226,26106.04622,29.97417857,0,3,1,6.839148781
439
+ 3.222831189,43071.83399,27.40206494,0,1,2,17.01150231
440
+ 5.068629869,32765.16978,38.86023362,0,4,5,51.4514559
441
+ 23.70873165,36962.47819,16.78626746,0,4,4,21.44168794
442
+ 11.21438181,20630.27471,39.56155467,0,1,4,58.40967562
443
+ 13.23305063,21032.93926,38.87717431,0,3,4,58.80432575
444
+ 21.09322616,33374.76649,51.42840332,0,3,5,16.82828521
445
+ 23.89808471,44499.6573,55.51804783,0,1,5,3.417715113
446
+ 14.40243233,46557.16636,33.10515697,0,2,5,42.59873377
447
+ 19.68689788,22601.19703,25.23299521,0,4,1,27.60820366
448
+ 8.51964356,36150.89306,22.78208915,0,1,2,6.944417794
449
+ 8.032111021,47396.89059,19.09355869,0,1,4,42.74384632
450
+ 10.26218558,31639.18263,11.28591219,0,3,1,2.839319638
451
+ 16.46766704,44886.97494,27.51953479,0,4,4,52.34160474
452
+ 16.69040885,24010.86963,23.40411845,0,3,1,9.180499218
453
+ 8.250506997,48061.83469,63.08579099,0,1,5,8.511400846
454
+ 4.063828255,42542.69953,39.7894031,0,2,4,11.36466558
455
+ 15.5451139,48240.03178,27.79479122,0,4,5,57.65571515
456
+ 3.95149483,40298.51701,41.08665326,0,1,4,41.89699225
457
+ 14.30532769,30902.11796,15.93421198,0,1,3,48.28092059
458
+ 16.94870895,48145.43518,33.3104937,0,2,1,24.46046416
459
+ 17.11406442,28267.00329,43.69535632,0,1,3,20.84245092
460
+ 18.32985855,29965.17566,35.86717641,0,1,1,14.8345564
461
+ 21.0757667,41024.25254,30.90139164,0,4,2,29.35823367
462
+ 12.49905682,42959.48899,64.64935817,0,4,4,33.55789528
463
+ 17.79573136,47882.10452,58.17794144,0,4,2,38.27471578
464
+ 15.24097291,26144.07227,43.53413962,0,3,2,16.16999174
465
+ 5.292718615,43939.94106,40.45450959,0,2,1,56.97059077
466
+ 1.590861832,42156.3816,43.37764735,0,4,3,11.01575139
467
+ 7.544451871,21913.83669,19.47479015,0,3,5,57.97353731
468
+ 11.18977525,31616.69424,30.52285137,0,3,1,1.876511228
469
+ 15.19343672,28482.96371,10.24919337,0,2,5,29.38641582
470
+ 16.47010718,9392.698572,35.10612414,1,2,2,20.66870245
471
+ 20.5490646,29134.71273,38.95240129,0,1,5,27.49828142
472
+ 5.513651484,49488.40639,20.98173496,0,3,3,34.66921016
473
+ 3.887645168,39283.3224,62.94964429,0,4,1,26.69976032
474
+ 23.16003879,41530.28473,59.83891282,0,2,3,25.47596417
475
+ 3.485024866,49298.52912,49.16216442,0,3,3,44.23068151
476
+ 12.00529461,31316.05347,38.40303784,0,4,1,41.46670023
477
+ 14.46796576,44055.20505,57.5882421,0,3,3,50.16300564
478
+ 13.45161168,33035.9289,15.4471836,0,2,4,15.74757989
479
+ 2.978739612,46086.01015,53.05905827,0,1,4,36.96218208
480
+ 2.311619022,25419.72624,15.1147864,0,3,4,35.52649234
481
+ 3.426118832,48430.58531,53.27000777,0,4,5,22.81453084
482
+ 14.46851673,26563.15125,23.71875139,0,2,3,29.67739497
483
+ 13.51993242,29786.61521,57.99416312,0,1,1,19.51314646
484
+ 6.376038217,42685.76702,61.81039935,0,4,1,9.368819459
485
+ 15.66880526,31812.65425,16.48137026,0,4,5,13.61427143
486
+ 19.86155255,38246.3413,34.37250949,0,4,1,7.225305138
487
+ 1.956316076,33337.20503,63.91628056,0,3,4,41.30110494
488
+ 23.14356675,5004.540764,35.55422385,1,2,3,39.98291619
489
+ 12.74170153,23300.03075,57.22220878,1,4,1,57.08577592
490
+ 12.4569887,42271.09811,41.55769676,0,3,1,44.05102641
491
+ 16.85413948,26849.93757,29.39380475,0,2,2,4.070704487
492
+ 6.803174323,21753.73962,24.28966266,0,2,2,53.17046526
493
+ 8.078730822,28997.88192,19.58371619,0,3,2,51.89783461
494
+ 15.11367177,34229.25995,51.42249691,0,3,3,27.15589893
495
+ 21.65251175,25040.60157,30.03280889,0,4,4,35.56591998
496
+ 19.62826167,30643.91869,37.14270135,0,2,4,58.58862162
497
+ 14.41199504,31999.49055,36.06255236,0,1,4,32.40595747
498
+ 17.79509951,21721.75531,40.90490467,0,3,3,54.41398787
499
+ 9.390916815,37499.21745,57.3378973,0,2,3,5.899286711
500
+ 15.74220715,46530.30284,63.90214258,0,2,2,12.27206025
501
+ 11.7221766,24552.28739,31.23229989,0,4,1,27.00454166
502
+ 5.368531139,37934.2404,55.16857856,0,1,5,35.50065169
503
+ 17.1399803,39942.17615,51.5780843,0,4,5,49.15744326
504
+ 13.79868666,32580.04041,40.69115023,0,1,1,59.30746479
505
+ 9.248296385,41032.92762,62.49468719,0,1,2,23.94223829
506
+ 21.96080137,32321.9463,19.42704739,0,3,1,3.797522532
507
+ 1.48148471,35139.11864,14.22805999,0,2,1,48.26911869
508
+ 8.262782479,20220.59134,56.67583212,0,2,5,36.65846243
509
+ 2.308272149,40751.60342,33.03153955,0,2,2,12.71402867
510
+ 18.63012827,39840.28037,56.27722469,0,1,3,30.19487894
511
+ 17.14847144,21152.02632,58.9061417,0,2,5,44.79630449
512
+ 8.621101031,31047.50322,11.56008284,0,3,3,46.45429544
513
+ 16.54024675,49370.95522,58.35749149,0,1,2,30.47748631
514
+ 10.1065562,32622.09688,33.55418616,0,1,1,19.00765728
515
+ 18.3938372,35058.36215,37.09465187,0,1,4,58.45081234
516
+ 11.44622424,47297.90474,43.71577313,0,1,4,40.13839405
517
+ 13.10832634,21877.58822,60.80380858,1,3,2,25.08601364
518
+ 10.48243374,41517.85628,9.089617382,0,2,4,17.54733023
519
+ 22.49878806,37462.10393,35.18007769,0,3,3,44.135076
520
+ 6.770219297,43934.14044,47.20015642,0,2,4,15.28882329
521
+ 3.77028882,45930.98799,49.19429659,0,3,2,11.13088935
522
+ 14.4444916,33706.64215,47.4743486,0,4,2,32.26019945
523
+ 23.29440168,34253.25095,15.66280238,0,4,3,18.10925617
524
+ 9.690301916,31581.52807,25.06554854,0,4,4,38.10540355
525
+ 2.426845563,49001.1842,28.44678495,0,4,4,7.958381818
526
+ 8.804021148,40914.98763,53.85292084,0,1,1,39.64152517
527
+ 12.63642805,22496.69199,23.88931273,0,1,5,17.71449515
528
+ 4.724229153,45888.7887,20.01018294,0,3,2,1.014333132
529
+ 16.13635102,34416.79823,62.57453709,0,3,1,8.489227403
530
+ 23.94823595,22062.61719,8.505207251,0,4,2,56.08330729
531
+ 13.95349187,19845.84463,64.87578877,1,2,1,52.17392371
532
+ 11.39000795,36491.51765,46.57881403,0,4,5,14.78293289
533
+ 9.158756805,32514.92837,55.22275007,0,3,1,46.92394237
534
+ 10.21619584,46332.48215,24.79330559,0,4,2,36.905724
535
+ 3.648362103,26119.83811,8.815970827,0,3,4,58.28337798
536
+ 21.31529283,44793.93828,50.05856256,0,4,2,23.04273391
537
+ 10.54099008,36730.66074,55.54624203,0,2,3,41.20617614
538
+ 9.907311463,21638.3091,50.20713952,0,4,3,4.748607623
539
+ 16.17287628,49155.35904,16.14246638,0,2,3,24.37859346
540
+ 11.16676868,35378.45732,50.94539157,0,2,3,57.86524201
541
+ 15.90936978,28726.30563,51.82859858,0,3,5,32.79987159
542
+ 2.413264673,34082.20526,45.53680939,0,2,4,17.9848907
543
+ 19.71563691,45735.73351,51.66860817,0,1,4,8.095986825
544
+ 22.63159092,29147.09373,56.21752206,0,1,2,42.50817207
545
+ 15.92160388,25780.29189,42.97543451,0,1,1,39.04650656
546
+ 19.14255401,8356.291913,13.0504687,1,3,2,23.0623841
547
+ 22.95501636,23872.13741,35.79478515,0,1,2,57.10553976
548
+ 4.482943231,28935.78195,12.42601208,0,3,2,2.309965966
549
+ 11.98057236,43401.44261,31.22995277,0,3,2,18.91970725
550
+ 14.84155488,34148.79096,31.20506988,0,1,4,55.34309969
551
+ 14.37994289,26824.44811,11.76256112,0,2,1,19.93519086
552
+ 10.23362092,24985.54999,27.88277044,0,4,2,15.8829172
553
+ 8.775941415,29981.8843,14.32689163,0,3,4,32.51900419
554
+ 3.916952519,48197.73119,54.06940697,0,1,2,49.60430325
555
+ 13.80566725,7407.271524,62.01821748,1,3,3,7.085658892
556
+ 10.01007636,30290.80138,12.12203909,0,2,2,56.63822187
557
+ 4.741198747,49643.72228,62.44156682,0,2,2,47.90929999
558
+ 17.89041056,34556.91367,37.78686616,0,1,5,11.69376508
559
+ 5.803111516,25374.91755,25.07524364,0,3,3,43.00846971
560
+ 2.347002022,46734.20275,12.38113829,0,4,1,23.71256464
561
+ 13.70442117,33810.66119,36.53558328,0,4,2,24.73538601
562
+ 14.69028385,41290.14031,53.28738604,0,3,1,47.21411545
563
+ 21.05400041,49511.3475,48.3039292,0,3,3,57.80430367
564
+ 14.27057709,21505.26051,10.86288265,0,3,2,8.472696498
565
+ 8.946059636,22900.83679,12.15540462,0,1,5,25.81718398
566
+ 19.46927328,41613.70224,30.96377711,0,2,4,29.37555488
567
+ 13.42641454,28454.7501,24.83155816,0,4,5,23.2023972
568
+ 2.823446547,25529.04191,21.24590641,0,1,5,22.4281583
569
+ 14.01456289,21180.85639,24.01725644,0,3,4,4.083851386
570
+ 21.88442654,20655.40136,53.79851637,0,3,4,41.44677861
571
+ 10.11519039,43234.56809,60.96599906,0,2,4,26.73665061
572
+ 7.815493444,24033.00282,31.09085359,0,3,2,12.49283436
573
+ 4.299797511,34031.61848,59.64832712,0,1,1,49.14597664
574
+ 4.483247005,38176.90446,26.32525497,0,1,1,48.35108414
575
+ 10.95374414,32850.55402,35.15690822,0,2,4,29.69710606
576
+ 14.70063596,20813.19957,20.88365288,0,1,5,18.60858901
577
+ 2.854154319,43505.04508,44.5071344,0,4,3,38.64747082
578
+ 22.61007755,20315.39674,63.80192356,0,1,4,18.00097601
579
+ 18.3680732,42856.69683,42.39910647,0,1,5,14.92446789
580
+ 14.49235012,46799.13481,28.39540279,0,1,5,44.10036459
581
+ 20.05408943,32985.19763,44.92559442,0,4,2,3.379189144
582
+ 2.844295407,36066.27419,15.00647867,0,3,2,6.591339791
583
+ 11.96729419,34309.50178,58.65356758,0,1,1,1.400502267
584
+ 15.49214192,31676.25856,36.67578519,0,2,1,10.023648
585
+ 20.05901684,46622.73606,33.6129353,0,4,3,5.070410514
586
+ 19.01979988,46818.00532,41.39429294,0,2,3,59.7523397
587
+ 7.372592743,30074.49094,43.61268018,0,1,3,18.03978823
588
+ 22.6402439,38452.09262,12.09122095,0,2,3,9.806812747
589
+ 3.859954353,47807.22686,46.90918161,0,1,5,15.21877922
590
+ 21.12807323,48861.75497,21.79010578,0,4,5,56.23140872
591
+ 23.33162574,35268.77121,48.69530005,0,2,1,31.68945316
592
+ 5.074531034,33584.74416,54.88448328,0,2,2,22.52426768
593
+ 17.60751948,28360.82948,53.82563493,0,1,2,52.53009602
594
+ 1.926566932,48766.1562,39.49255514,0,1,4,24.30442596
595
+ 10.32472498,24284.42913,37.64968386,0,3,5,20.76550563
596
+ 12.87451588,37394.88728,16.1439298,0,1,3,13.22966883
597
+ 14.3548402,28476.50803,52.19473057,0,1,3,23.29001261
598
+ 22.52655671,28370.24871,23.47033469,0,3,4,19.684377
599
+ 16.46176581,32149.05762,36.3116391,0,1,2,40.9609336
600
+ 12.06939288,49963.78807,24.2036233,0,1,3,18.08537958
601
+ 19.62252583,21648.98921,15.62821669,0,3,2,35.05478504
602
+ 22.86225488,37356.25455,43.88478872,0,4,4,4.887637737
603
+ 1.50752142,22477.16474,11.09692598,0,4,2,12.28606025
604
+ 23.59798926,24635.6249,50.67277835,0,1,2,25.03691103
605
+ 2.981039198,33186.88825,26.10244733,0,1,2,56.28000598
606
+ 19.73089592,35313.80394,8.007677501,0,3,3,23.25786716
607
+ 7.447699309,27763.77225,37.13436094,0,3,1,40.18037315
608
+ 4.040132128,29493.71393,10.67055879,0,4,5,16.05014612
609
+ 11.11667971,22609.01061,23.7416648,0,2,2,8.494980982
610
+ 15.81100133,29096.59352,48.29765978,0,1,2,4.453659405
611
+ 9.761892436,48996.75318,11.57330843,0,2,2,19.01939641
612
+ 2.255004391,48341.22145,55.84229406,0,2,1,10.30200058
613
+ 14.78760621,30046.3487,8.217735592,0,3,4,11.26661643
614
+ 21.75527535,37125.61809,22.06896054,0,1,3,45.88479919
615
+ 9.813953417,47287.86173,50.2315312,0,1,1,1.934446186
616
+ 5.975653886,20185.51468,26.02739853,0,3,2,33.91079742
617
+ 11.17927896,44320.50711,13.80786612,0,2,1,23.38822146
618
+ 2.112342354,48451.08658,28.53333337,0,3,1,30.88644999
619
+ 19.87121444,22739.77203,23.41241732,0,4,1,34.206984
620
+ 20.00583865,24843.43058,56.03457842,0,1,4,19.48869397
621
+ 14.52401066,48822.38908,25.8608313,0,4,5,18.41142275
622
+ 9.121366446,36126.20431,52.96915089,0,3,3,44.27070911
623
+ 19.39501738,20425.89903,58.83671225,0,1,4,52.4053423
624
+ 13.75780157,27728.95996,32.72759292,0,4,4,12.36231632
625
+ 19.9964431,33498.31023,59.8667592,0,4,5,6.759506872
626
+ 15.52355223,40787.26733,29.50712908,0,2,4,53.96995258
627
+ 19.04132588,41994.81134,62.95246974,0,1,1,21.64383236
628
+ 14.76694884,49862.64539,13.0895029,0,4,1,33.52772605
629
+ 10.52524457,28625.33598,47.16009819,0,4,2,22.21304553
630
+ 23.03275478,36146.60306,36.14740031,0,2,4,8.422337844
631
+ 13.44561797,35999.70644,30.09598191,0,4,2,35.77056705
632
+ 14.91705893,32169.35778,44.06457709,0,4,3,31.98331025
633
+ 6.071215707,25074.85682,48.11985422,0,4,1,12.97661999
634
+ 15.38996816,49323.6969,8.24870637,0,2,3,27.66027363
635
+ 23.61691658,6352.094989,17.51632606,1,4,5,48.01990274
636
+ 14.14697594,45599.5156,48.64362489,0,4,1,35.83514008
637
+ 5.251283153,34949.99085,45.98396857,0,1,3,6.718122297
638
+ 2.370503239,38097.36268,63.06470779,0,1,5,33.33326834
639
+ 14.90026435,26817.83161,51.37915404,0,2,3,42.46155966
640
+ 18.5649222,36313.61211,62.19404434,0,3,5,47.76950094
641
+ 13.03823153,40849.10399,48.04480859,0,1,5,38.11793714
642
+ 6.215770382,35455.01085,24.98897589,0,2,1,10.00683171
643
+ 16.33000546,49708.55512,14.00580057,0,4,2,42.90022273
644
+ 2.842431712,38340.24969,52.56396021,0,4,5,40.96316892
645
+ 11.16217521,31129.21138,44.71650562,0,2,5,2.441186093
646
+ 4.757435443,43164.56083,10.74660271,0,4,1,25.29311239
647
+ 17.36057433,19480.58378,28.52287656,1,4,2,20.64406457
648
+ 5.242558174,25113.87535,62.53758948,0,3,4,40.49531179
649
+ 5.652760831,31535.47441,36.52286531,0,4,4,13.75201293
650
+ 9.894262312,30408.76851,32.66556075,0,1,2,35.86144346
651
+ 2.176454729,32954.21285,34.08887749,0,4,3,54.87915175
652
+ 10.14924914,43453.39478,19.90631527,0,2,1,48.07033996
653
+ 12.76927128,34525.19096,29.01630874,0,3,2,55.36600644
654
+ 12.11576855,43820.43351,29.07935559,0,4,4,58.73185205
655
+ 9.804034309,37294.66709,10.98423615,0,2,5,1.629537493
656
+ 18.01821552,23576.15921,51.75135483,1,1,4,37.74173531
657
+ 20.30037199,30262.08304,31.74075262,0,4,5,25.35752447
658
+ 4.342513926,44299.36231,54.86425355,0,3,1,51.73141493
659
+ 12.63102511,27148.69012,56.46983606,0,4,5,15.85706742
660
+ 2.42695259,26731.48939,20.08366112,0,2,2,58.88073986
661
+ 2.636580614,35135.08772,45.46914848,0,3,5,37.2087702
662
+ 14.19891454,27825.47738,34.92006284,0,4,3,47.81870673
663
+ 14.01842377,20272.61105,58.16893406,0,1,2,6.050373617
664
+ 21.1947708,39448.92582,20.29721871,0,3,1,11.8240152
665
+ 13.83109454,35332.81464,46.63535242,0,1,2,45.74581671
666
+ 23.07519347,33365.53443,42.64187097,0,2,3,2.2972048
667
+ 2.119981513,49009.05544,24.83218422,0,4,1,49.19580117
668
+ 3.25417675,37135.97552,15.78625151,0,1,4,1.822992103
669
+ 2.013514816,45902.94733,45.14346635,0,2,3,39.7700115
670
+ 5.261367558,24183.50709,50.10005511,0,4,1,22.54054129
671
+ 13.44921255,37076.19389,25.99075859,0,2,5,37.07819631
672
+ 15.84816017,46039.3888,44.75545843,0,1,2,42.06585301
673
+ 2.048458633,37493.17272,30.52246173,0,2,1,21.20603793
674
+ 22.75733047,41540.42534,48.65279235,0,3,2,7.369784439
675
+ 20.36980366,31161.30703,19.35527067,0,4,5,5.109106197
676
+ 7.82208709,22773.48551,58.74225515,0,2,1,35.74722533
677
+ 2.775313195,33737.00885,24.382347,0,1,4,31.71866587
678
+ 4.907402684,31834.38173,28.9638013,0,4,3,44.78985954
679
+ 14.95990339,13610.71642,11.31124127,1,3,1,31.07439509
680
+ 3.919721516,46011.80547,14.3561961,0,4,5,25.92430161
681
+ 3.835526924,27319.85907,37.40412845,0,3,1,24.48714352
682
+ 12.92691884,49002.02683,23.25278257,0,4,2,2.560367253
683
+ 6.654491177,48744.14359,55.62237287,0,2,1,20.7978749
684
+ 9.23721348,27428.94929,8.837523153,0,2,4,58.58518929
685
+ 23.75862267,30441.55866,29.60836077,0,1,1,52.539779
686
+ 16.73922773,46866.67223,27.22869617,0,3,3,20.43330596
687
+ 22.82655534,48998.66663,9.101674962,0,1,1,35.6799289
688
+ 4.278800766,33303.17309,15.08899874,0,2,2,50.99050799
689
+ 9.789207921,29629.91137,31.57763247,0,3,1,27.03518587
690
+ 13.75883148,35782.8204,36.09328131,0,1,4,2.059513961
691
+ 2.765435423,40633.07619,31.04451559,0,1,5,5.396117247
692
+ 1.096311203,28204.23546,38.26344767,0,1,1,31.11317056
693
+ 16.41998968,42094.19524,41.92250547,0,1,1,58.19692316
694
+ 15.76184889,21549.72071,8.565657804,0,1,3,41.89936299
695
+ 10.44814263,21478.83478,34.45342409,0,3,4,5.184055295
696
+ 12.26181375,31588.63115,62.91945673,0,1,5,12.17229167
697
+ 10.55518901,30604.19576,37.58483997,0,3,2,46.50806545
698
+ 1.552116031,29468.99646,46.61990925,0,1,1,50.64494584
699
+ 8.713546943,34730.59288,25.7762399,0,2,5,37.44398059
700
+ 5.072891874,29622.00747,52.11750855,0,2,2,39.02096701
701
+ 3.263756349,25767.73755,52.05652447,0,1,4,2.671909573
702
+ 23.00456998,29284.08647,37.71387472,0,4,4,49.88328532
703
+ 14.78527368,49247.12818,63.63278793,0,2,5,47.50823728
704
+ 17.75909454,32333.40251,15.15636224,0,3,2,16.2830735
705
+ 8.253366095,33341.38331,8.966016899,0,1,4,42.81192648
706
+ 10.04238398,37296.3497,51.89901807,0,3,5,13.67642178
707
+ 6.44925186,49500.22305,54.00786563,0,4,4,3.379479502
708
+ 3.230210432,27248.03829,14.85181206,0,1,4,12.86705601
709
+ 5.117727661,40438.46332,23.13822148,0,2,4,11.53910373
710
+ 19.34282304,41479.54081,9.000484712,0,3,3,20.92713181
711
+ 16.58355352,38219.7325,24.71859081,0,2,2,25.68852033
712
+ 17.65152303,7382.81344,52.06908681,1,2,5,31.0037946
713
+ 13.57936415,22657.35019,37.52375008,0,1,5,29.12847865
714
+ 11.92706708,40306.16827,27.84144881,0,3,2,16.96980175
715
+ 19.94296359,15247.85641,29.18913981,1,4,2,2.721069446
716
+ 22.21598852,21922.84133,8.077156666,0,2,2,36.70099571
717
+ 2.672903799,37884.27949,25.08863007,0,1,2,24.34894496
718
+ 7.453818704,42187.43532,44.84814827,0,1,1,18.62165729
719
+ 21.01412564,11976.63815,63.5290833,1,4,5,52.14952153
720
+ 9.048570632,26329.09672,56.28246714,0,1,1,45.05785516
721
+ 18.99579368,45248.66023,9.344912985,0,2,4,51.60816735
722
+ 23.82783626,20910.61417,59.21795447,0,1,4,30.20398489
723
+ 6.535421839,45314.38591,52.64230956,0,3,5,8.724943875
724
+ 21.10906869,33809.87152,52.48143904,0,3,4,42.07706473
725
+ 20.10432349,26527.6382,34.10575826,0,2,2,13.15835302
726
+ 6.167120146,24013.34435,30.6863017,0,3,1,27.03255048
727
+ 10.18464934,48599.21503,25.26619654,0,4,2,4.835046548
728
+ 10.42247599,22179.96763,11.74460696,0,1,4,49.69319574
729
+ 23.49907538,25972.8026,21.00941572,0,2,1,8.430497822
730
+ 5.160513732,30434.67042,22.05640719,0,4,2,54.28275292
731
+ 19.38063334,38607.04547,35.59239297,0,1,5,51.98960553
732
+ 8.686432426,36344.03256,50.60433118,0,4,2,28.4103831
733
+ 17.81001795,23769.30477,35.00716022,0,2,4,44.91772405
734
+ 10.6602531,28691.83951,11.29713362,0,2,1,37.54725936
735
+ 14.30227455,27162.55313,62.59401866,0,4,5,48.3814629
736
+ 20.15007079,26406.28563,61.73528619,0,1,1,26.47650095
737
+ 19.49580799,32334.33009,52.75580396,0,3,5,47.36270057
738
+ 20.92773322,41120.94358,64.50569039,0,3,1,28.28495221
739
+ 2.371198483,26523.58797,39.03549297,0,4,4,56.51698148
740
+ 16.91716272,31000.36639,62.87779596,0,2,5,29.69595395
741
+ 4.219709665,31727.52281,12.30948732,0,1,3,28.29453704
742
+ 10.56382706,49293.69588,28.84380265,0,1,1,47.31923983
743
+ 13.61753355,48639.99071,20.84673228,0,2,1,49.47308381
744
+ 10.26300815,44004.12748,19.1624858,0,3,2,33.71485774
745
+ 12.95106854,27249.76762,16.02576809,0,1,5,11.608071
746
+ 23.9201051,22041.11942,43.47757757,0,4,3,28.59773563
747
+ 4.109998215,29507.12413,52.53534578,0,3,4,58.11627767
748
+ 16.53548904,28003.83648,40.96300877,0,4,2,45.42618539
749
+ 10.10802223,35177.774,16.37687445,0,3,3,48.85859363
750
+ 4.059225375,43331.8373,54.23405489,0,2,5,2.93670443
751
+ 4.650945558,45749.68817,44.24941831,0,4,3,28.6037083
752
+ 22.83328144,45345.92256,30.12544525,0,4,1,39.15064689
753
+ 21.23083442,30790.26684,46.42542685,0,2,1,49.97192833
754
+ 21.86196509,40617.0303,22.81458382,0,4,2,3.907145561
755
+ 23.80525384,29385.02593,27.67595588,0,3,5,51.29389425
756
+ 5.79125397,21827.1663,60.29669306,0,1,5,33.35565527
757
+ 9.175104759,41046.95797,24.55875682,0,1,3,12.64152426
758
+ 16.39203769,20294.82658,34.78933129,0,2,2,19.71805777
759
+ 12.12745842,32275.47762,58.73804483,0,3,5,17.63384355
760
+ 10.62295927,29881.37808,48.34011697,0,4,1,32.32068302
761
+ 9.229658735,40801.32616,11.54558389,0,4,3,4.049455115
762
+ 14.65695453,42987.36418,16.39627352,0,1,4,10.35363335
763
+ 14.23864886,29122.83908,8.446970674,0,4,1,48.94888956
764
+ 4.713530183,44348.81043,43.96254204,0,1,2,8.823635454
765
+ 11.85001626,28817.50282,33.51738525,0,2,5,14.7703799
766
+ 13.72985085,33389.7908,15.6536665,0,4,1,27.74886573
767
+ 14.11659959,41810.55581,62.6022449,0,3,4,31.5101324
768
+ 5.832465437,40787.60688,38.19060426,0,4,4,46.0530183
769
+ 18.07224202,49621.05417,21.78791776,0,1,2,27.39261317
770
+ 1.579579181,26754.83073,36.53440356,0,2,1,47.60201264
771
+ 9.161132677,39869.53741,46.73835897,0,1,3,37.48243212
772
+ 18.94489092,38290.45725,12.34563542,0,4,1,7.672884626
773
+ 13.96953718,47452.95363,23.65811746,0,1,4,10.22355487
774
+ 7.006735948,24690.86425,53.99703298,0,1,2,26.7849898
775
+ 16.98392174,42553.94491,34.20089761,0,2,2,24.41647419
776
+ 14.04999172,37048.05849,39.1548971,0,3,3,44.86911191
777
+ 19.31360302,28239.97609,32.67051213,0,4,2,35.26349125
778
+ 17.90557928,34233.04742,10.50229673,0,2,2,23.89985178
779
+ 15.03248221,30013.29791,17.44773745,0,3,2,11.07485672
780
+ 12.22808653,37379.40208,33.39587341,0,1,2,14.38338904
781
+ 4.06318889,46670.01832,19.92357153,0,3,2,34.95456009
782
+ 7.010659411,37079.14953,10.84880463,0,1,4,25.98288949
783
+ 10.63928788,38951.67255,56.08803085,0,1,1,31.38072302
784
+ 14.7838836,35562.43541,63.92797416,0,2,5,11.06996868
785
+ 12.81528067,21534.96519,53.20914382,0,1,2,40.11146889
786
+ 7.628244097,26985.75671,56.66574054,0,2,5,51.29978571
787
+ 1.148680229,34760.05188,21.79269643,0,3,5,16.2184309
788
+ 12.4135021,29307.05851,62.7557376,0,3,4,29.99933932
789
+ 7.569397737,25452.90793,19.22476515,0,1,2,58.55611241
790
+ 17.89599092,35123.98526,62.23149919,0,2,5,20.66126269
791
+ 1.550764101,29684.2938,64.70469976,0,1,2,36.10773435
792
+ 14.45103966,27743.2293,48.56819993,0,2,3,6.321546312
793
+ 21.04641699,12496.49849,63.92519271,1,2,3,36.75488515
794
+ 22.65390746,33919.56252,40.46376744,0,2,3,6.288274625
795
+ 5.011413144,23998.52774,22.79388164,0,3,5,57.59489068
796
+ 11.84793829,47832.37199,32.90876548,0,2,3,43.22409647
797
+ 3.102705812,46031.06294,41.83297332,0,3,4,47.5014275
798
+ 15.39068398,41228.61757,12.16564904,0,2,4,42.54215729
799
+ 13.66222865,23186.41483,43.47356608,0,3,5,35.79401313
800
+ 18.93469335,17426.61248,63.9271367,1,4,2,27.54485408
801
+ 10.36940706,43276.31763,18.83613604,0,3,1,51.64402941
802
+ 12.99167149,44238.71984,53.17792163,0,3,2,57.45330439
803
+ 21.62121714,43032.93853,59.75023125,0,1,2,30.4431342
804
+ 5.514768953,23418.97661,61.79099079,0,2,2,55.9605679
805
+ 22.8523173,19374.36227,62.72772945,1,4,2,35.41664169
806
+ 1.518983769,39317.23871,37.72319941,0,1,1,32.76466703
807
+ 21.80889417,7212.071977,63.70655076,1,2,3,27.81448124
808
+ 20.81829235,45029.74387,51.16668181,0,4,2,13.34797793
809
+ 14.27369289,37320.60204,17.21527187,0,4,4,1.219459988
810
+ 21.48514448,39392.22126,35.18330607,0,4,5,15.24886738
811
+ 14.72974014,33750.90438,48.944877,0,2,5,31.07366712
812
+ 19.63370507,35861.2196,22.098398,0,4,4,23.41763718
813
+ 10.88248736,21031.30575,44.51506546,0,4,2,27.46062845
814
+ 18.24677842,39168.89066,45.99622731,0,1,1,58.85824495
815
+ 21.99995374,22443.41639,17.2738854,0,3,3,21.66606987
816
+ 14.15882817,33050.43783,40.20986281,0,2,5,27.67879342
817
+ 5.158058463,28779.65293,51.98273393,0,3,3,8.87741753
818
+ 7.182345146,36362.83851,36.43705288,0,1,1,3.829506214
819
+ 5.568825269,24002.49007,8.689877656,0,3,5,46.74916136
820
+ 6.677711635,40749.30206,8.515192738,0,4,3,34.67030733
821
+ 8.047555335,43850.34611,28.35067261,0,4,1,45.88158356
822
+ 13.7680771,24900.63349,60.7930619,0,2,3,9.841905134
823
+ 14.52380748,23129.39262,21.03457644,0,2,2,34.7378975
824
+ 10.80949419,24365.46448,44.15873027,0,2,1,15.96490599
825
+ 15.15467794,46313.82686,20.65832463,0,1,3,14.4556472
826
+ 2.876223289,35845.67785,26.33518941,0,2,2,1.680034829
827
+ 3.019129452,34017.82988,56.33839911,0,1,2,8.690102691
828
+ 4.952077588,23201.35457,49.54510221,0,4,1,19.24681858
829
+ 12.91578245,39486.6802,13.43775881,0,3,4,7.895846875
830
+ 5.92187761,46039.98776,32.4360152,0,4,5,59.16564798
831
+ 7.51554128,44845.57608,9.662676528,0,4,1,19.20932626
832
+ 21.08457738,5711.804616,35.41073836,1,4,2,8.689112855
833
+ 10.20719523,38781.64127,45.75873867,0,2,2,12.74585646
834
+ 19.67292085,22258.3615,14.75464955,0,2,1,35.67449047
835
+ 1.318829126,23371.91082,24.46644152,0,2,4,4.778567908
836
+ 15.93571287,21479.26961,30.67668765,0,3,5,51.88361368
837
+ 16.38416021,27724.37649,60.41326841,0,2,3,57.72848107
838
+ 19.36908295,24069.84401,64.61553588,0,1,1,15.55809782
839
+ 22.4533176,27801.44121,10.55994404,0,4,5,27.55562085
840
+ 1.463170526,31551.74167,51.37745164,0,3,5,2.305932611
841
+ 12.66726626,24886.60605,29.18827178,1,3,2,38.80190326
842
+ 4.537025227,48289.48352,30.37011662,0,3,3,25.42163743
843
+ 21.38193976,38311.28179,50.99310348,0,3,1,16.9334495
844
+ 11.54592693,45049.21357,60.35030663,0,3,2,13.20338953
845
+ 13.9855847,41310.54502,62.20292989,0,1,2,39.58736158
846
+ 12.08253555,9654.566978,40.8959874,1,4,4,25.55680483
847
+ 16.24962569,23375.09392,28.35590608,0,3,5,11.59989805
848
+ 16.6185319,42077.61219,52.89029789,0,4,1,41.65743581
849
+ 22.43002928,28252.96645,22.30707885,0,2,1,34.3392647
850
+ 23.9298366,39173.23541,40.15221964,0,3,1,51.66165124
851
+ 17.5636216,30228.85921,28.43897231,0,3,2,7.931078034
852
+ 7.606087812,49930.1679,45.4279712,0,4,4,47.35432951
853
+ 21.23430525,27906.54438,21.70273803,0,3,3,40.73749042
854
+ 2.132081034,28373.91412,18.92079791,0,3,2,7.785645273
855
+ 6.314859616,47920.16767,60.33962794,0,3,3,41.99442306
856
+ 21.6398715,20739.33214,13.80280993,0,1,4,27.05129004
857
+ 6.892404832,40900.66964,36.83960074,0,1,5,56.75410235
858
+ 6.074974371,41759.79172,20.58860863,0,3,1,9.355069686
859
+ 4.176881742,45091.35732,10.21900791,0,1,3,38.92717006
860
+ 20.75957043,49233.17932,10.05307264,0,2,2,23.53682268
861
+ 12.50133996,49268.05757,17.98764101,0,3,3,57.45206556
862
+ 16.52254367,21958.30273,57.40591021,0,2,5,20.46851882
863
+ 6.498526834,44655.17968,24.10128841,0,4,1,57.76896866
864
+ 18.43492968,39389.29801,62.17617272,0,2,3,54.18766521
865
+ 18.48012753,39420.26403,41.1524707,0,4,5,16.95261088
866
+ 8.189754573,23850.4978,32.88700882,0,3,2,48.40896483
867
+ 10.44570056,39505.61499,41.06506358,0,4,4,59.74064791
868
+ 5.822876381,49474.47742,37.45180796,0,4,2,15.90711713
869
+ 23.46090354,47354.55709,51.25024646,0,2,1,26.76985299
870
+ 15.84885927,33173.91982,24.10227576,0,2,2,31.69066346
871
+ 22.49450926,21198.4392,28.12387232,0,2,2,18.40093701
872
+ 7.977867503,30868.8047,58.96337524,0,3,2,1.900625539
873
+ 18.983491,44791.70935,61.94802125,0,3,2,13.98762541
874
+ 18.61474881,41044.97304,58.87581897,0,2,1,17.63427618
875
+ 16.90807999,33780.30967,31.90853566,0,3,5,27.17238238
876
+ 23.20969643,21653.04605,52.48083367,0,3,3,11.09504623
877
+ 10.05177273,32806.84183,35.1511368,0,4,4,52.52200704
878
+ 4.006498443,34070.74819,36.35977157,0,4,3,42.58975683
879
+ 16.44930376,47856.66599,19.66676622,0,2,3,6.026828176
880
+ 17.77412541,39232.58311,41.69442729,0,3,4,39.35545462
881
+ 14.22639319,29349.5875,18.60974617,0,4,1,39.56573211
882
+ 5.756811542,35759.54793,26.89918785,0,1,4,57.41704924
883
+ 12.60248666,21981.65923,56.73672149,0,3,1,1.17823141
884
+ 8.180363941,49177.04707,19.80332113,0,1,3,15.62801454
885
+ 10.83173015,30801.79706,12.05602225,0,2,1,55.73969728
886
+ 15.84002724,34336.02065,11.93343278,0,3,5,55.99590101
887
+ 18.13993908,30658.51923,61.62471767,0,2,4,43.51459874
888
+ 6.341167891,40475.3519,36.89446496,0,2,5,46.18003594
889
+ 14.54936733,6076.610498,31.33648809,1,2,3,6.397494518
890
+ 10.32248329,39866.96475,54.22007912,0,2,4,31.31867425
891
+ 7.190716133,41459.66932,55.64207789,0,2,3,14.66339649
892
+ 5.568527119,28044.29689,26.93488889,0,3,5,11.69186457
893
+ 19.01790606,29470.05443,47.53622928,0,1,3,48.80789465
894
+ 21.91281197,23757.53693,51.9540005,0,3,4,8.967250064
895
+ 9.515340271,39665.29916,45.31508915,0,3,4,28.65044658
896
+ 18.27941526,21193.91382,16.64091926,0,3,3,56.37611614
897
+ 19.96228313,8797.506582,57.92533576,1,4,4,11.96409128
898
+ 21.63089166,37930.47007,38.72838951,0,4,4,58.701612
899
+ 20.46085684,46090.87655,24.1009283,0,3,3,59.66299116
900
+ 5.204707936,46724.96445,32.23800657,0,1,5,33.78141213
901
+ 22.99471327,33471.13281,10.14155215,0,3,4,11.03842167
902
+ 9.66355813,33779.11065,15.28840767,0,2,4,19.65018839
903
+ 8.802535207,22309.07549,51.63616478,0,4,4,26.04507713
904
+ 2.435920527,40122.95076,8.000663181,0,4,4,19.25012924
905
+ 12.18199404,44637.78261,31.74425222,0,3,3,17.55749921
906
+ 3.50731316,27339.65164,37.78308032,0,3,5,58.36155284
907
+ 6.716415881,32639.02645,11.11416194,0,1,5,10.04852482
908
+ 8.289680301,45165.1801,63.46545529,0,1,1,44.26634661
909
+ 19.60300544,46313.61403,20.88914388,0,2,4,1.024207077
910
+ 21.13623896,27295.39941,25.33932702,0,3,4,36.17477749
911
+ 7.956725302,36517.11904,25.3247232,0,3,1,55.04063723
912
+ 20.75005679,25774.79452,21.13374974,0,3,5,17.98811155
913
+ 20.71631156,23056.36292,8.084007859,0,4,2,11.94603254
914
+ 18.58697186,47333.61504,49.57265326,0,2,2,39.4491079
915
+ 4.373987622,44615.22513,63.11019349,0,1,5,43.04348303
916
+ 16.87742548,42801.04705,20.78472855,0,4,4,41.59380062
917
+ 22.73555349,21462.98956,45.79368993,0,4,2,19.83969592
918
+ 16.69843101,45311.86378,50.28809064,0,4,3,15.87994173
919
+ 8.627187529,33141.32228,56.36024661,0,4,3,57.3992092
920
+ 12.5133243,30271.8537,32.08986423,0,2,1,55.47467099
921
+ 15.851676,36394.29083,25.26706162,0,3,5,27.98167998
922
+ 12.7431194,22740.59728,26.54182264,0,1,5,50.9254381
923
+ 3.75891766,42699.39648,48.61941599,0,1,2,27.68708518
924
+ 16.93436987,5779.156927,54.55642959,1,4,4,26.03895207
925
+ 8.177473027,37473.05231,18.35199616,0,4,5,10.24901391
926
+ 19.4551087,25597.70291,29.14363746,0,2,2,26.15795733
927
+ 20.81261506,25646.96599,59.41059121,0,4,2,32.70759938
928
+ 4.157900752,25527.45955,53.98153062,0,1,5,2.327755318
929
+ 22.90489531,21710.07163,64.13692896,0,1,2,27.20266731
930
+ 8.562920211,37633.95348,50.9921508,0,3,1,47.10001689
931
+ 16.23321599,45210.44533,30.41212646,0,3,1,41.12271733
932
+ 18.29197836,20524.94642,41.66635654,0,1,5,38.47426234
933
+ 19.6747634,21470.13935,45.67781249,0,1,4,54.66377028
934
+ 22.75224315,37183.1083,12.47198153,0,2,1,2.06388858
935
+ 8.197149605,44584.87056,39.03632667,0,2,1,33.86580759
936
+ 16.82887511,12836.21789,48.43128722,1,1,5,47.45358206
937
+ 20.74564845,29802.16672,17.53731433,0,4,3,32.93878758
938
+ 4.028798779,30488.43196,52.49600789,0,3,2,20.1098376
939
+ 17.21594085,37890.39492,41.27504849,0,1,1,9.03878334
940
+ 18.06548778,28226.38667,62.27664178,0,4,3,53.36622124
941
+ 16.58763455,20798.25098,10.418071,0,1,4,28.77498115
942
+ 6.803238931,42056.85554,23.12358984,0,3,3,52.80879662
943
+ 21.07999238,38769.49739,42.28857112,0,3,5,35.56527522
944
+ 4.883744658,37422.67997,24.903915,0,2,4,40.77733488
945
+ 2.233600205,42250.52387,48.72217735,0,2,4,48.45140811
946
+ 17.50169478,40256.92513,51.2633011,0,2,3,39.89017713
947
+ 11.93594145,26176.45965,13.84340995,0,2,5,24.4852356
948
+ 20.52925446,30574.43141,37.28970446,0,3,1,31.30436965
949
+ 9.818134244,23783.30425,37.00676167,0,4,3,48.37968596
950
+ 4.870407536,23895.7104,29.0490039,0,4,1,23.04536749
951
+ 20.23817043,40285.87239,61.17671717,0,4,3,23.89916595
952
+ 13.62492153,45758.00321,55.16786023,0,1,1,6.645716847
953
+ 5.351503225,48881.97752,47.74093477,0,2,3,54.96048168
954
+ 17.56883197,38850.41353,48.71661824,0,2,4,6.371142582
955
+ 12.75561369,25523.0397,34.3178202,0,3,2,39.24434147
956
+ 14.89919859,23169.51036,60.4966877,0,3,4,25.37847425
957
+ 11.60066178,44385.52915,47.59193935,0,1,1,24.93154403
958
+ 20.06030663,37362.69429,49.55192051,0,1,5,41.09355836
959
+ 22.61504266,21048.03569,57.11638152,1,4,4,4.958530225
960
+ 20.07994246,38548.55063,23.62208059,0,3,2,36.02864119
961
+ 17.3159429,43443.86266,54.00304152,0,2,1,46.54987537
962
+ 22.74675418,11824.889,19.12871437,1,4,4,49.86455272
963
+ 3.42423168,41098.31683,27.68449128,0,2,3,11.06129297
964
+ 22.91010317,13883.66814,27.12979589,1,4,5,53.94772395
965
+ 20.29363882,45033.74759,63.7759517,0,2,1,39.71261942
966
+ 16.43329855,21686.41716,56.82262175,0,1,4,53.02943325
967
+ 17.03155582,42425.6191,47.96668211,0,4,1,29.63156231
968
+ 9.348605329,45525.58272,49.44223541,0,2,3,32.97996736
969
+ 21.22342979,28584.05127,40.03814928,0,2,2,55.22325834
970
+ 17.68354681,39213.41822,61.98416803,0,3,4,54.09466306
971
+ 3.58958073,29424.86192,36.28674891,0,4,1,27.44149153
972
+ 10.37889479,48739.13199,29.68950946,0,2,1,17.19034527
973
+ 21.38674833,21002.70717,17.29301421,0,3,1,24.19705564
974
+ 2.085524139,37393.78531,52.81372218,0,2,2,23.87809538
975
+ 23.11258225,24995.10641,49.86330413,0,4,1,17.92514562
976
+ 16.62073907,41366.60616,29.9082372,0,1,5,16.84619666
977
+ 15.52164749,25276.7085,9.436024596,0,4,5,51.80271325
978
+ 7.222862899,27061.49786,55.82284768,0,4,1,5.12185332
979
+ 6.722211141,34831.12433,8.650825057,0,2,5,40.97905974
980
+ 22.66733905,47445.0485,48.11088744,0,2,4,11.19638304
981
+ 7.860266447,26351.43116,63.30465525,0,4,5,21.32584771
982
+ 15.76653692,18511.73547,32.9466957,1,3,1,4.631039014
983
+ 18.83662182,24327.20063,21.39348408,0,3,3,16.92844457
984
+ 10.04180136,42769.30496,48.17764757,0,3,4,46.3368432
985
+ 4.202843628,35018.58597,54.57630409,0,2,2,53.21590415
986
+ 1.246451847,47353.55337,39.14652802,0,2,1,14.07010196
987
+ 13.51791875,24398.43671,63.12101069,0,4,2,28.34932252
988
+ 23.44291283,39377.83679,10.94511713,0,1,3,28.74911883
989
+ 9.490132458,26986.37438,36.7733491,0,2,2,52.54094304
990
+ 9.215423682,24702.81286,48.951875,0,4,1,46.00514412
991
+ 8.914107211,40502.88245,57.17050686,0,4,1,31.27155588
992
+ 12.97250826,37810.5386,18.21756988,0,1,3,40.57408483
993
+ 11.13529232,42644.82375,53.60019846,0,3,1,50.99394818
994
+ 21.23803641,29283.87729,39.50430332,0,4,3,1.562798288
995
+ 12.62697821,33439.14969,30.60355987,0,4,3,24.77051183
996
+ 12.67843028,32164.15029,15.50775663,0,4,1,54.52734687
997
+ 4.182546679,49522.68392,57.32185826,0,3,5,44.67846882
998
+ 23.60300944,28767.4687,16.96457287,0,3,3,33.02764109
999
+ 6.74937649,28844.51715,25.65790797,0,1,1,32.2490271
1000
+ 2.687373552,30636.44626,24.53259532,0,2,5,21.51843576
1001
+ 16.25236373,36937.82244,57.67059995,0,3,3,31.71071148
data/raw/Computer_Durability_Plus.csv ADDED
The diff for this file is too large to render. See raw diff
 
models/best_model_info.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "winner": "XGBoost",
3
+ "run_id": "222e3e5354e24caa80b6addf170c7739",
4
+ "registry_version": 1,
5
+ "metrics": {
6
+ "roc_auc": 0.8413560591871138,
7
+ "avg_precision": 0.28931499498300856,
8
+ "f1": 0.3764705882352941
9
+ },
10
+ "all_metrics": {
11
+ "RandomForest": {
12
+ "roc_auc": 0.8269807079977524,
13
+ "avg_precision": 0.24554784524827208,
14
+ "f1": 0.3140495867768595
15
+ },
16
+ "XGBoost": {
17
+ "roc_auc": 0.8413560591871138,
18
+ "avg_precision": 0.28931499498300856,
19
+ "f1": 0.3764705882352941
20
+ }
21
+ }
22
+ }
models/feature_meta.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "feature_cols": [
3
+ "Hours Used Per Day",
4
+ "Cost",
5
+ "User Age",
6
+ "Primary Usage",
7
+ "Brand",
8
+ "Computer Age (Months)"
9
+ ],
10
+ "target_col": "Needs Replacement",
11
+ "scaler_path": "/home/karl-joseph-cacao/Documents/MSDS2026/Term_04/MLOps_MSDS2026/MLOps_end2end_MSDS2026/models/scaler.joblib"
12
+ }
models/optuna_trials.csv ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ number,value,datetime_start,datetime_complete,duration,params_colsample_bytree,params_gamma,params_learning_rate,params_max_depth,params_min_child_weight,params_n_estimators,params_reg_alpha,params_reg_lambda,params_subsample,state
2
+ 0,0.1898637299496884,2026-05-31 15:36:16.578732,2026-05-31 15:36:17.003259,0 days 00:00:00.424527,0.5780093202212182,0.2904180608409973,0.1205712628744377,10,2,250,0.08499808989182997,0.0006440507553993703,0.8394633936788146,COMPLETE
3
+ 1,0.18258175254449607,2026-05-31 15:36:17.003313,2026-05-31 15:36:17.082029,0 days 00:00:00.078716,0.6061695553391381,0.9170225492671691,0.2708160864249968,3,2,383,2.716051144654844e-06,0.00015777981883364995,0.9329770563201687,COMPLETE
4
+ 2,0.2369980882131971,2026-05-31 15:36:17.082077,2026-05-31 15:36:17.148082,0 days 00:00:00.066005,0.6460723242676091,2.28034992108518,0.08012737503998542,5,4,273,0.019116469627784252,3.9572205641009174e-07,0.6557975442608167,COMPLETE
5
+ 3,0.23354413552023795,2026-05-31 15:36:17.148116,2026-05-31 15:36:17.260518,0 days 00:00:00.112402,0.5852620618436457,4.7444276862666666,0.011711509955524094,7,1,306,0.530953226900921,0.02932100047183291,0.8430179407605753,COMPLETE
6
+ 4,0.23962087875157065,2026-05-31 15:36:17.260553,2026-05-31 15:36:17.316920,0 days 00:00:00.056367,0.5610191174223894,0.17194260557609198,0.1024932221692416,3,5,222,0.1881755597772026,1.1755466083160747e-06,0.7760609974958406,COMPLETE
7
+ 5,0.28667717461871667,2026-05-31 15:36:17.316957,2026-05-31 15:36:17.421954,0 days 00:00:00.104997,0.5924272277627636,3.8756641168055728,0.05864129169696527,5,10,365,0.32808889626606236,0.14408501080722544,0.8186841117373118,COMPLETE
8
+ 6,0.25540877113288846,2026-05-31 15:36:17.421987,2026-05-31 15:36:17.590549,0 days 00:00:00.168562,0.522613644455269,1.9433864484474102,0.01351182947645082,10,4,339,1.481809088646707e-06,0.04264813784432918,0.6783931449676581,COMPLETE
9
+ 7,0.22473300885806227,2026-05-31 15:36:17.590588,2026-05-31 15:36:17.647970,0 days 00:00:00.057382,0.9010984903770198,4.9344346830025865,0.06333268775321842,5,1,243,0.015064619068942013,3.8879928024075543e-07,0.6563696899899051,COMPLETE
10
+ 8,0.26037160449907365,2026-05-31 15:36:17.648004,2026-05-31 15:36:17.687360,0 days 00:00:00.039356,0.8856351733429728,1.7923286427213632,0.11069143219393454,9,1,102,8.451863533931625e-08,0.08032068562667222,0.8916028672163949,COMPLETE
11
+ 9,0.2634981661447631,2026-05-31 15:36:17.687395,2026-05-31 15:36:17.796768,0 days 00:00:00.109373,0.6625916610133735,3.1877873567760657,0.012413189635294229,5,8,349,0.12522814303053625,5.994036749692399e-05,0.7243929286862649,COMPLETE
12
+ 10,0.2631726307572251,2026-05-31 15:36:17.796802,2026-05-31 15:36:17.951005,0 days 00:00:00.154203,0.7640858921203038,3.651834476035047,0.03001215871999436,7,10,486,0.000708454929345622,0.5743386936170548,0.9729161367647149,COMPLETE
13
+ 11,0.26603700011816767,2026-05-31 15:36:17.951047,2026-05-31 15:36:18.066038,0 days 00:00:00.114991,0.7171781358782056,3.428985852396359,0.03057610208137913,5,9,438,0.0007202349503619245,2.541136388519596e-05,0.7392194339652385,COMPLETE
14
+ 12,0.26924362098676174,2026-05-31 15:36:18.066076,2026-05-31 15:36:18.180931,0 days 00:00:00.114855,0.7542782371321683,3.8010404681426615,0.03338034147970685,6,10,443,0.00027677678129142897,1.5223416071433098e-08,0.7458004804578023,COMPLETE
15
+ 13,0.26031906342822275,2026-05-31 15:36:18.180969,2026-05-31 15:36:18.288627,0 days 00:00:00.107658,0.8160782391706531,4.133292234546421,0.03531369483454838,6,7,407,4.354583380764534e-05,1.887357440280478e-08,0.6037493867846113,COMPLETE
16
+ 14,0.2496807049617076,2026-05-31 15:36:18.288666,2026-05-31 15:36:18.402156,0 days 00:00:00.113490,0.9966762831864868,2.7939929152165717,0.04815035925095873,6,10,492,0.0010872340147197433,0.004359299029555911,0.8038102230086545,COMPLETE
17
+ 15,0.2791457576443677,2026-05-31 15:36:18.402191,2026-05-31 15:36:18.539029,0 days 00:00:00.136838,0.7242740284450381,4.109844361539446,0.02028573521720704,8,7,435,1.3307088829376484e-08,3.868159214922418e-08,0.8795233936488246,COMPLETE
18
+ 16,0.2738912655179476,2026-05-31 15:36:18.539071,2026-05-31 15:36:18.674124,0 days 00:00:00.135053,0.6836874653341911,4.369794306410908,0.020397706135341725,8,7,383,1.6510195320654132e-08,5.251280804878214e-06,0.8920538105669723,COMPLETE
19
+ 17,0.25417907776052284,2026-05-31 15:36:18.674167,2026-05-31 15:36:18.762207,0 days 00:00:00.088040,0.8132394567758081,2.838091932582077,0.018661763995585987,8,7,185,4.598257659375889e-06,0.9522581473958315,0.8749004528350904,COMPLETE
20
+ 18,0.2356488662792346,2026-05-31 15:36:18.762243,2026-05-31 15:36:18.839684,0 days 00:00:00.077441,0.5094030062553871,4.353215554408531,0.04969388325976179,4,8,328,2.9904280092387287e-07,0.0013509039128217649,0.9694880010155159,COMPLETE
21
+ 19,0.22775512996274952,2026-05-31 15:36:18.839723,2026-05-31 15:36:18.924300,0 days 00:00:00.084577,0.639553048746349,3.9411181890882356,0.1991266186754082,8,6,426,3.454555947497455e-05,5.246279583118166e-08,0.8109069179221944,COMPLETE
22
+ 20,0.28931499498300856,2026-05-31 15:36:18.924341,2026-05-31 15:36:19.061684,0 days 00:00:00.137343,0.7020843771180812,3.176227806450042,0.02261987722445193,9,9,378,1.2387044364130546e-08,6.840793596052189e-06,0.9184453934740648,COMPLETE
23
+ 21,0.28581974382822006,2026-05-31 15:36:19.061724,2026-05-31 15:36:19.257097,0 days 00:00:00.195373,0.7149872794581777,3.250454518489347,0.02032786129497231,9,9,377,1.3767884945287468e-08,5.30621871476353e-06,0.9253052475673965,COMPLETE
24
+ 22,0.28920056877860617,2026-05-31 15:36:19.257139,2026-05-31 15:36:19.406564,0 days 00:00:00.149425,0.6883898835864328,3.115044878544297,0.023136549204546084,9,9,364,1.7566366830590038e-07,8.359410053058924e-06,0.938363077009676,COMPLETE
25
+ 23,0.26653265451306396,2026-05-31 15:36:19.406602,2026-05-31 15:36:19.495439,0 days 00:00:00.088837,0.6160774117802493,2.8617253865724335,0.04000479078782372,9,9,296,1.7977504933100885e-07,1.7227894475773726e-05,0.9997173908335091,COMPLETE
26
+ 24,0.26794141321867904,2026-05-31 15:36:19.495474,2026-05-31 15:36:19.588458,0 days 00:00:00.092984,0.8039619302221802,2.290670043607198,0.06487473530802493,10,8,356,6.037447074927862e-07,0.00023039318949668005,0.9225443295963883,COMPLETE
27
+ 25,0.2665346426703458,2026-05-31 15:36:19.588498,2026-05-31 15:36:19.684495,0 days 00:00:00.095997,0.6821269497054445,1.4635679205345227,0.02604698774930044,4,9,314,1.140881083355269e-05,4.0565145398254975e-06,0.8465055785221199,COMPLETE
28
+ 26,0.2820373576224597,2026-05-31 15:36:19.684531,2026-05-31 15:36:19.835644,0 days 00:00:00.151113,0.5361187515937964,3.410347487890499,0.01606989528103154,9,10,395,6.362577991228303e-08,0.004737188696616971,0.9403252601806452,COMPLETE
29
+ 27,0.2714868590236465,2026-05-31 15:36:19.835683,2026-05-31 15:36:19.976217,0 days 00:00:00.140534,0.6149440016111111,2.558894363479297,0.024851203526807926,7,8,459,0.004920491332843422,8.99350063281423e-07,0.957152195047184,COMPLETE
30
+ 28,0.25661956243485884,2026-05-31 15:36:19.976258,2026-05-31 15:36:20.070753,0 days 00:00:00.094495,0.6919072523191407,2.9963426885215694,0.04094162942045279,4,9,364,4.861837982039971e-08,3.375170910136555e-05,0.7803890844735994,COMPLETE
31
+ 29,0.2158726038429907,2026-05-31 15:36:20.070791,2026-05-31 15:36:20.145960,0 days 00:00:00.075169,0.5554549411439629,1.0270497742181646,0.15921728208236582,10,6,280,7.26811739217153e-07,0.0006337665565356945,0.8557310125499158,COMPLETE
pyproject.toml ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mlops-pipeline"
7
+ version = "0.1.0"
8
+ description = "End-to-end MLOps pipeline: Dagster + MLflow + Evidently + FastAPI + Gradio"
9
+ requires-python = ">=3.12,<3.13"
10
+ dependencies = [
11
+ # Orchestration
12
+ "dagster>=1.9.0",
13
+ "dagster-webserver>=1.9.0",
14
+ # ML
15
+ "scikit-learn>=1.5.0",
16
+ "xgboost>=2.1.0",
17
+ "optuna>=3.6.0",
18
+ "imbalanced-learn>=0.12.0",
19
+ # Experiment tracking
20
+ "mlflow>=2.15.0",
21
+ # Monitoring / drift
22
+ "evidently>=0.4.33",
23
+ # Serving
24
+ "fastapi>=0.112.0",
25
+ "uvicorn[standard]>=0.30.0",
26
+ "pydantic>=2.8.0",
27
+ # Frontend
28
+ "gradio>=4.42.0",
29
+ "requests>=2.32.0",
30
+ # Data
31
+ "pandas>=2.2.0",
32
+ "numpy>=1.26.0",
33
+ "scipy>=1.14.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "pytest>=8.0.0",
39
+ "ipykernel>=6.29.0",
40
+ "jupyter>=1.0.0",
41
+ ]
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/mlops_pipeline"]
45
+
46
+ [tool.dagster]
47
+ module_name = "mlops_pipeline.definitions"
scripts/run_pipeline.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Run the full Dagster asset pipeline programmatically (no web UI needed).
3
+ Executes all assets in dependency order:
4
+ raw_data β†’ augmented_data β†’ train_test_split_asset
5
+ β†’ baseline_rf_model + tuned_xgb_model β†’ best_model_info
6
+ β†’ data_quality_report + data_drift_report + model_eval_report
7
+ """
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
12
+
13
+ from dagster import materialize
14
+
15
+ from mlops_pipeline.assets.data_assets import (
16
+ augmented_data,
17
+ raw_data,
18
+ train_test_split_asset,
19
+ )
20
+ from mlops_pipeline.assets.evaluation_assets import (
21
+ data_drift_report,
22
+ data_quality_report,
23
+ model_eval_report,
24
+ )
25
+ from mlops_pipeline.assets.training_assets import (
26
+ baseline_rf_model,
27
+ best_model_info,
28
+ tuned_xgb_model,
29
+ )
30
+ from mlops_pipeline.config import MLFLOW_EXPERIMENT, MLFLOW_TRACKING_URI
31
+ from mlops_pipeline.resources import MLflowResource
32
+
33
+ ASSETS = [
34
+ raw_data,
35
+ augmented_data,
36
+ train_test_split_asset,
37
+ baseline_rf_model,
38
+ tuned_xgb_model,
39
+ best_model_info,
40
+ data_quality_report,
41
+ data_drift_report,
42
+ model_eval_report,
43
+ ]
44
+
45
+ if __name__ == "__main__":
46
+ print("=" * 60)
47
+ print("MLOps Pipeline: Computer Durability Classifier")
48
+ print("=" * 60)
49
+
50
+ result = materialize(
51
+ ASSETS,
52
+ resources={
53
+ "mlflow": MLflowResource(
54
+ tracking_uri=MLFLOW_TRACKING_URI,
55
+ experiment_name=MLFLOW_EXPERIMENT,
56
+ )
57
+ },
58
+ )
59
+
60
+ if result.success:
61
+ print("\nβœ“ Pipeline completed successfully!")
62
+ print(f" MLflow UI: mlflow ui --backend-store-uri {MLFLOW_TRACKING_URI}")
63
+ print(f" Reports : reports/")
64
+ print(f" Models : models/")
65
+ print("\nTo serve the model:")
66
+ print(" uv run uvicorn serving.api:app --host 0.0.0.0 --port 8000")
67
+ print(" uv run python app.py")
68
+ else:
69
+ print("\nβœ— Pipeline failed. Check logs above.")
70
+ sys.exit(1)
scripts/synthesize_data.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Synthesize ~2000 additional rows for Computer_Durability_Plus.csv.
3
+
4
+ Distribution matches the original 999-row dataset but introduces mild drift
5
+ so Evidently can detect it:
6
+ - Hours Used Per Day shifted +2h (heavier usage in new cohort)
7
+ - Cost shifted -$3000 (cheaper machines in new cohort)
8
+ - Class rate ~7% positive (up from 5%), still realistic
9
+
10
+ The synthesized rows are appended to the original data and saved as
11
+ Computer_Durability_Plus.csv at the project root.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import csv
16
+ import random
17
+ import math
18
+ from pathlib import Path
19
+
20
+ SEED = 42
21
+ N_SYNTH = 2000
22
+ ROOT = Path(__file__).parent.parent
23
+
24
+ # Original distribution parameters (derived from EDA)
25
+ ORIG = {
26
+ "hours_mean": 12.648, "hours_std": 6.558, "hours_min": 1.0, "hours_max": 24.0,
27
+ "cost_mean": 33789.0, "cost_std": 9647.0, "cost_min": 5000.0, "cost_max": 50000.0,
28
+ "age_mean": 36.637, "age_std": 16.560, "age_min": 8.0, "age_max": 65.0,
29
+ "comp_age_mean": 29.654, "comp_age_std": 16.862, "comp_age_min": 1.0, "comp_age_max": 60.0,
30
+ }
31
+ # Mild drift: heavier usage, cheaper machines
32
+ DRIFT = {
33
+ "hours_mean": 14.8, # +2.15h drift
34
+ "hours_std": 6.2,
35
+ "cost_mean": 30500.0, # -$3289 drift
36
+ "cost_std": 9200.0,
37
+ "age_mean": 36.637, # unchanged
38
+ "age_std": 16.560,
39
+ "comp_age_mean": 29.654,
40
+ "comp_age_std": 16.862,
41
+ }
42
+
43
+ HEADER = [
44
+ "Hours Used Per Day", "Cost", "User Age",
45
+ "Needs Replacement", "Primary Usage", "Brand", "Computer Age (Months)"
46
+ ]
47
+
48
+
49
+ def clamp(value: float, lo: float, hi: float) -> float:
50
+ return max(lo, min(hi, value))
51
+
52
+
53
+ def box_muller(mean: float, std: float, rng: random.Random) -> float:
54
+ u1, u2 = rng.random(), rng.random()
55
+ z = math.sqrt(-2 * math.log(max(u1, 1e-12))) * math.cos(2 * math.pi * u2)
56
+ return mean + std * z
57
+
58
+
59
+ def replacement_probability(hours: float, cost: float, user_age: float) -> float:
60
+ """
61
+ Signal calibrated to match observed replacement rates:
62
+ <12h/day β†’ ~0%, 12-16h β†’ ~8%, >16h β†’ ~11%
63
+ Cost modifier: cheaper machine β†’ higher risk.
64
+ Age modifier: older users slightly more at risk.
65
+ Overall positive rate: ~5-7% (original ~5%, drift cohort ~7%).
66
+ """
67
+ if hours < 12.0:
68
+ base = 0.002
69
+ elif hours < 16.0:
70
+ base = 0.07
71
+ else:
72
+ base = 0.11
73
+ # Cost: scale from 1.8 (cheapest, $5k) to 0.2 (most expensive, $50k)
74
+ cost_factor = 2.0 - 1.8 * (cost - 5000.0) / 45000.0
75
+ # User age: mild uplift for older users
76
+ age_factor = 1.0 + 0.01 * max(0.0, user_age - 40.0)
77
+ return min(base * cost_factor * age_factor, 0.85)
78
+
79
+
80
+ def synthesize(n: int, rng: random.Random) -> list[list]:
81
+ rows = []
82
+ for _ in range(n):
83
+ hours = clamp(box_muller(DRIFT["hours_mean"], DRIFT["hours_std"], rng), 1.0, 24.0)
84
+ cost = clamp(box_muller(DRIFT["cost_mean"], DRIFT["cost_std"], rng), 5000.0, 50000.0)
85
+ user_age = clamp(box_muller(DRIFT["age_mean"], DRIFT["age_std"], rng), 8.0, 65.0)
86
+ comp_age = clamp(box_muller(DRIFT["comp_age_mean"], DRIFT["comp_age_std"], rng), 1.0, 60.0)
87
+ primary_usage = rng.randint(1, 4)
88
+ brand = rng.randint(1, 5)
89
+ p_replace = replacement_probability(hours, cost, user_age)
90
+ needs_replacement = 1 if rng.random() < p_replace else 0
91
+ rows.append([
92
+ round(hours, 8), round(cost, 5), round(user_age, 8),
93
+ needs_replacement, primary_usage, brand, round(comp_age, 7)
94
+ ])
95
+ return rows
96
+
97
+
98
+ def main() -> None:
99
+ rng = random.Random(SEED)
100
+
101
+ src = ROOT / "Computer_Durability.csv"
102
+ dst = ROOT / "Computer_Durability_Plus.csv"
103
+
104
+ orig_rows = []
105
+ with src.open(encoding="utf-8-sig") as f:
106
+ reader = csv.reader(f)
107
+ next(reader) # skip header
108
+ for r in reader:
109
+ orig_rows.append([
110
+ float(r[0]), float(r[1]), float(r[2]),
111
+ int(r[3]), int(r[4]), int(r[5]), float(r[6])
112
+ ])
113
+
114
+ synth_rows = synthesize(N_SYNTH, rng)
115
+ all_rows = orig_rows + synth_rows
116
+
117
+ pos = sum(1 for r in all_rows if r[3] == 1)
118
+ print(f"Original rows : {len(orig_rows)}")
119
+ print(f"Synthesized : {len(synth_rows)}")
120
+ print(f"Total rows : {len(all_rows)}")
121
+ print(f"Positive class: {pos} ({100*pos/len(all_rows):.1f}%)")
122
+
123
+ with dst.open("w", newline="", encoding="utf-8") as f:
124
+ writer = csv.writer(f)
125
+ writer.writerow(HEADER)
126
+ writer.writerows(all_rows)
127
+
128
+ print(f"Saved β†’ {dst}")
129
+
130
+ # Also copy raw originals into data/raw/
131
+ raw_dir = ROOT / "data" / "raw"
132
+ raw_dir.mkdir(parents=True, exist_ok=True)
133
+ import shutil
134
+ shutil.copy(src, raw_dir / src.name)
135
+ shutil.copy(dst, raw_dir / dst.name)
136
+ print(f"Copied raw files β†’ {raw_dir}")
137
+
138
+
139
+ if __name__ == "__main__":
140
+ main()
serving/__init__.py ADDED
File without changes
serving/api.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI model serving endpoint.
3
+
4
+ Loads the champion model from MLflow Model Registry (or falls back to the
5
+ locally saved joblib file) and serves predictions.
6
+
7
+ Start with:
8
+ uv run uvicorn serving.api:app --host 0.0.0.0 --port 8000 --reload
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ import joblib
17
+ import mlflow
18
+ import numpy as np
19
+ from fastapi import FastAPI, HTTPException
20
+ from pydantic import BaseModel, Field
21
+
22
+ ROOT = Path(__file__).parent.parent
23
+ MODELS_DIR = ROOT / "models"
24
+ META_PATH = MODELS_DIR / "feature_meta.json"
25
+ INFO_PATH = MODELS_DIR / "best_model_info.json"
26
+
27
+ app = FastAPI(
28
+ title="Computer Durability Classifier",
29
+ description="Predicts whether a computer needs replacement based on usage patterns.",
30
+ version="1.0.0",
31
+ )
32
+
33
+ # ── Model Loading ─────────────────────────────────────────────────────────────
34
+
35
+ _model = None
36
+ _scaler = None
37
+ _feature_cols: list[str] = []
38
+
39
+
40
+ def _load_artifacts() -> None:
41
+ global _model, _scaler, _feature_cols
42
+
43
+ if META_PATH.exists():
44
+ meta = json.loads(META_PATH.read_text())
45
+ _feature_cols = meta["feature_cols"]
46
+ _scaler = joblib.load(meta["scaler_path"])
47
+ else:
48
+ raise RuntimeError("feature_meta.json not found β€” run the Dagster pipeline first.")
49
+
50
+ if INFO_PATH.exists():
51
+ info = json.loads(INFO_PATH.read_text())
52
+ winner = info.get("winner", "XGBoost")
53
+ if winner == "XGBoost":
54
+ model_path = MODELS_DIR / "xgb_tuned.joblib"
55
+ else:
56
+ model_path = MODELS_DIR / "rf_baseline.joblib"
57
+ _model = joblib.load(model_path)
58
+ else:
59
+ raise RuntimeError("best_model_info.json not found β€” run the Dagster pipeline first.")
60
+
61
+
62
+ @app.on_event("startup")
63
+ def startup_event() -> None:
64
+ _load_artifacts()
65
+
66
+
67
+ # ── Request / Response schemas ────────────────────────────────────────────────
68
+
69
+ class PredictionRequest(BaseModel):
70
+ hours_used_per_day: float = Field(..., ge=0.0, le=24.0, example=18.5)
71
+ cost: float = Field(..., ge=0.0, example=15000.0)
72
+ user_age: float = Field(..., ge=0.0, le=120.0, example=45.0)
73
+ primary_usage: int = Field(..., ge=1, le=4, example=2)
74
+ brand: int = Field(..., ge=1, le=5, example=3)
75
+ computer_age_months: float = Field(..., ge=0.0, example=36.0)
76
+
77
+
78
+ class PredictionResponse(BaseModel):
79
+ needs_replacement: bool
80
+ probability: float
81
+ model_version: Optional[str] = None
82
+
83
+
84
+ # ── Endpoints ─────────────────────────────────────────────────────────────────
85
+
86
+ @app.get("/health")
87
+ def health() -> dict:
88
+ return {"status": "ok", "model_loaded": _model is not None}
89
+
90
+
91
+ @app.get("/info")
92
+ def model_info() -> dict:
93
+ if INFO_PATH.exists():
94
+ return json.loads(INFO_PATH.read_text())
95
+ return {"error": "model info not available"}
96
+
97
+
98
+ @app.post("/predict", response_model=PredictionResponse)
99
+ def predict(request: PredictionRequest) -> PredictionResponse:
100
+ if _model is None or _scaler is None:
101
+ raise HTTPException(status_code=503, detail="Model not loaded")
102
+
103
+ # Assemble feature vector in the exact column order
104
+ row = np.array([[
105
+ request.hours_used_per_day,
106
+ request.cost,
107
+ request.user_age,
108
+ request.primary_usage,
109
+ request.brand,
110
+ request.computer_age_months,
111
+ ]])
112
+ row_scaled = _scaler.transform(row)
113
+ prob = float(_model.predict_proba(row_scaled)[0, 1])
114
+ label = prob >= 0.5
115
+
116
+ version = None
117
+ if INFO_PATH.exists():
118
+ info = json.loads(INFO_PATH.read_text())
119
+ version = f"{info.get('winner')} v{info.get('registry_version')}"
120
+
121
+ return PredictionResponse(
122
+ needs_replacement=label,
123
+ probability=round(prob, 4),
124
+ model_version=version,
125
+ )
126
+
127
+
128
+ @app.post("/predict/batch")
129
+ def predict_batch(requests: list[PredictionRequest]) -> list[PredictionResponse]:
130
+ if _model is None or _scaler is None:
131
+ raise HTTPException(status_code=503, detail="Model not loaded")
132
+
133
+ rows = np.array([
134
+ [r.hours_used_per_day, r.cost, r.user_age, r.primary_usage, r.brand, r.computer_age_months]
135
+ for r in requests
136
+ ])
137
+ rows_scaled = _scaler.transform(rows)
138
+ probs = _model.predict_proba(rows_scaled)[:, 1]
139
+
140
+ version = None
141
+ if INFO_PATH.exists():
142
+ info = json.loads(INFO_PATH.read_text())
143
+ version = f"{info.get('winner')} v{info.get('registry_version')}"
144
+
145
+ return [
146
+ PredictionResponse(
147
+ needs_replacement=bool(p >= 0.5),
148
+ probability=round(float(p), 4),
149
+ model_version=version,
150
+ )
151
+ for p in probs
152
+ ]
src/__init__.py ADDED
File without changes
src/mlops_pipeline/__init__.py ADDED
File without changes
src/mlops_pipeline/assets/__init__.py ADDED
File without changes
src/mlops_pipeline/assets/data_assets.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dagster data assets:
3
+ raw_data β†’ load original 999-row CSV
4
+ augmented_data β†’ load 3000-row Plus CSV (original + 2000 synthetic)
5
+ train_test_split β†’ stratified split with SMOTE oversampling on train set
6
+ """
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+ from dagster import AssetExecutionContext, asset
14
+ from imblearn.over_sampling import SMOTE
15
+ from sklearn.model_selection import train_test_split
16
+ from sklearn.preprocessing import StandardScaler
17
+
18
+ from mlops_pipeline.config import (
19
+ FEATURE_COLS,
20
+ MODELS_DIR,
21
+ ORIG_CSV,
22
+ PLUS_CSV,
23
+ TARGET_COL,
24
+ )
25
+
26
+
27
+ @asset(group_name="data")
28
+ def raw_data(context: AssetExecutionContext) -> pd.DataFrame:
29
+ """Load the original 999-row Computer_Durability.csv."""
30
+ df = pd.read_csv(ORIG_CSV)
31
+ context.log.info(f"Loaded raw data: {df.shape}, positives={df[TARGET_COL].sum()}")
32
+ context.add_output_metadata(
33
+ {
34
+ "num_rows": int(len(df)),
35
+ "num_positives": int(df[TARGET_COL].sum()),
36
+ "positive_rate": float(df[TARGET_COL].mean()),
37
+ "columns": df.columns.tolist(),
38
+ }
39
+ )
40
+ return df
41
+
42
+
43
+ @asset(group_name="data")
44
+ def augmented_data(context: AssetExecutionContext) -> pd.DataFrame:
45
+ """Load the 3000-row Computer_Durability_Plus.csv (original + 2000 synthetic)."""
46
+ df = pd.read_csv(PLUS_CSV)
47
+ context.log.info(f"Loaded augmented data: {df.shape}, positives={df[TARGET_COL].sum()}")
48
+ context.add_output_metadata(
49
+ {
50
+ "num_rows": int(len(df)),
51
+ "num_positives": int(df[TARGET_COL].sum()),
52
+ "positive_rate": float(df[TARGET_COL].mean()),
53
+ "synthetic_rows": int(len(df) - 999),
54
+ }
55
+ )
56
+ return df
57
+
58
+
59
+ @asset(group_name="data")
60
+ def train_test_split_asset(
61
+ context: AssetExecutionContext,
62
+ augmented_data: pd.DataFrame,
63
+ ) -> dict[str, Any]:
64
+ """
65
+ Stratified 80/20 split, then SMOTE on the training set.
66
+ Returns a dict with X_train, X_test, y_train, y_test as numpy arrays
67
+ plus the fitted StandardScaler.
68
+ """
69
+ X = augmented_data[FEATURE_COLS].values
70
+ y = augmented_data[TARGET_COL].values
71
+
72
+ X_train, X_test, y_train, y_test = train_test_split(
73
+ X, y, test_size=0.2, random_state=42, stratify=y
74
+ )
75
+
76
+ # Scale numeric features (fit only on train)
77
+ scaler = StandardScaler()
78
+ X_train_scaled = scaler.fit_transform(X_train)
79
+ X_test_scaled = scaler.transform(X_test)
80
+
81
+ # SMOTE: oversample minority class on train set only
82
+ smote = SMOTE(random_state=42, k_neighbors=5)
83
+ X_train_res, y_train_res = smote.fit_resample(X_train_scaled, y_train)
84
+
85
+ context.log.info(
86
+ f"Train (after SMOTE): {X_train_res.shape}, pos={y_train_res.sum()}"
87
+ )
88
+ context.log.info(
89
+ f"Test (no SMOTE): {X_test_scaled.shape}, pos={y_test.sum()}"
90
+ )
91
+
92
+ # Persist scaler for serving
93
+ MODELS_DIR.mkdir(parents=True, exist_ok=True)
94
+ import joblib
95
+ joblib.dump(scaler, MODELS_DIR / "scaler.joblib")
96
+
97
+ # Persist feature metadata for serving
98
+ meta = {
99
+ "feature_cols": FEATURE_COLS,
100
+ "target_col": TARGET_COL,
101
+ "scaler_path": str(MODELS_DIR / "scaler.joblib"),
102
+ }
103
+ (MODELS_DIR / "feature_meta.json").write_text(json.dumps(meta, indent=2))
104
+
105
+ context.add_output_metadata(
106
+ {
107
+ "train_rows": int(len(X_train_res)),
108
+ "test_rows": int(len(X_test_scaled)),
109
+ "train_positives_after_smote": int(y_train_res.sum()),
110
+ "test_positives": int(y_test.sum()),
111
+ }
112
+ )
113
+
114
+ return {
115
+ "X_train": X_train_res,
116
+ "X_test": X_test_scaled,
117
+ "y_train": y_train_res,
118
+ "y_test": y_test,
119
+ "feature_cols": FEATURE_COLS,
120
+ }
src/mlops_pipeline/assets/evaluation_assets.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dagster evaluation assets using Evidently 0.7+ API:
3
+ data_quality_report β†’ DataSummaryPreset on augmented_data
4
+ data_drift_report β†’ DataDriftPreset: original vs synthetic cohort
5
+ model_eval_report β†’ ClassificationPreset on test predictions
6
+ All HTML reports saved to reports/.
7
+ """
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import joblib
12
+ import numpy as np
13
+ import pandas as pd
14
+ from dagster import AssetExecutionContext, asset
15
+ from evidently import DataDefinition, Dataset
16
+ from evidently.core.datasets import BinaryClassification
17
+ from evidently.core.report import Report
18
+ from evidently.presets import ClassificationPreset, DataDriftPreset, DataSummaryPreset
19
+
20
+ from mlops_pipeline.config import (
21
+ FEATURE_COLS,
22
+ MODELS_DIR,
23
+ NUMERIC_COLS,
24
+ CATEGORICAL_COLS,
25
+ ORIG_CSV,
26
+ PLUS_CSV,
27
+ REPORTS_DIR,
28
+ TARGET_COL,
29
+ )
30
+
31
+
32
+ def _save_report(snapshot, name: str) -> Path:
33
+ REPORTS_DIR.mkdir(parents=True, exist_ok=True)
34
+ out = REPORTS_DIR / f"{name}.html"
35
+ snapshot.save_html(str(out))
36
+ return out
37
+
38
+
39
+ def _base_definition() -> DataDefinition:
40
+ return DataDefinition(
41
+ numerical_columns=NUMERIC_COLS,
42
+ categorical_columns=CATEGORICAL_COLS + [TARGET_COL],
43
+ )
44
+
45
+
46
+ @asset(group_name="evaluation")
47
+ def data_quality_report(
48
+ context: AssetExecutionContext,
49
+ augmented_data: pd.DataFrame,
50
+ ) -> str:
51
+ """Evidently data summary/quality report on the full augmented dataset."""
52
+ dd = _base_definition()
53
+ ds = Dataset.from_pandas(augmented_data, data_definition=dd)
54
+ report = Report([DataSummaryPreset()])
55
+ snapshot = report.run(ds, None)
56
+ out = _save_report(snapshot, "data_quality")
57
+ context.log.info(f"Data quality report β†’ {out}")
58
+ context.add_output_metadata({"report_path": str(out)})
59
+ return str(out)
60
+
61
+
62
+ @asset(group_name="evaluation")
63
+ def data_drift_report(
64
+ context: AssetExecutionContext,
65
+ ) -> str:
66
+ """Evidently drift report: original CSV (reference) vs 2000 synthetic rows (current)."""
67
+ orig = pd.read_csv(ORIG_CSV)
68
+ plus = pd.read_csv(PLUS_CSV)
69
+ synth = plus.iloc[len(orig):]
70
+
71
+ dd = _base_definition()
72
+ ref_ds = Dataset.from_pandas(orig, data_definition=dd)
73
+ cur_ds = Dataset.from_pandas(synth, data_definition=dd)
74
+
75
+ report = Report([DataDriftPreset()])
76
+ snapshot = report.run(cur_ds, ref_ds)
77
+ out = _save_report(snapshot, "data_drift")
78
+ context.log.info(f"Data drift report β†’ {out}")
79
+ context.add_output_metadata({"report_path": str(out)})
80
+ return str(out)
81
+
82
+
83
+ @asset(group_name="evaluation")
84
+ def model_eval_report(
85
+ context: AssetExecutionContext,
86
+ train_test_split_asset: dict[str, Any],
87
+ best_model_info: dict[str, Any],
88
+ ) -> str:
89
+ """Evidently classification report on the test set using the best model."""
90
+ winner = best_model_info["winner"]
91
+ model_path = MODELS_DIR / ("xgb_tuned.joblib" if winner == "XGBoost" else "rf_baseline.joblib")
92
+ model = joblib.load(model_path)
93
+
94
+ X_test = train_test_split_asset["X_test"]
95
+ y_test = train_test_split_asset["y_test"]
96
+
97
+ y_prob = model.predict_proba(X_test)[:, 1]
98
+ y_pred = (y_prob >= 0.5).astype(int)
99
+
100
+ test_df = pd.DataFrame(X_test, columns=FEATURE_COLS)
101
+ test_df[TARGET_COL] = y_test
102
+ test_df["prediction_proba"] = y_prob
103
+ test_df["prediction_labels"] = y_pred
104
+
105
+ dd = DataDefinition(
106
+ numerical_columns=FEATURE_COLS,
107
+ classification=[
108
+ BinaryClassification(
109
+ target=TARGET_COL,
110
+ prediction_labels="prediction_labels",
111
+ prediction_probas="prediction_proba",
112
+ )
113
+ ],
114
+ )
115
+ ds = Dataset.from_pandas(test_df, data_definition=dd)
116
+ report = Report([ClassificationPreset()])
117
+ snapshot = report.run(ds, None)
118
+ out = _save_report(snapshot, "model_evaluation")
119
+
120
+ context.log.info(f"Model eval report β†’ {out}")
121
+ context.add_output_metadata(
122
+ {
123
+ "report_path": str(out),
124
+ "winner_model": winner,
125
+ "test_rows": len(y_test),
126
+ }
127
+ )
128
+ return str(out)
src/mlops_pipeline/assets/training_assets.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dagster training assets:
3
+ baseline_rf_model β†’ RandomForest with class_weight='balanced'
4
+ tuned_xgb_model β†’ XGBoost tuned via Optuna (30 trials)
5
+ best_model_info β†’ compare RF vs XGBoost, pick winner
6
+ All runs logged to MLflow.
7
+ """
8
+ import json
9
+ from typing import Any
10
+
11
+ import mlflow
12
+ import mlflow.sklearn
13
+ import mlflow.xgboost
14
+ import numpy as np
15
+ import optuna
16
+ import xgboost as xgb
17
+ from dagster import AssetExecutionContext, asset
18
+ from sklearn.ensemble import RandomForestClassifier
19
+ from sklearn.metrics import (
20
+ average_precision_score,
21
+ f1_score,
22
+ roc_auc_score,
23
+ )
24
+
25
+ from mlops_pipeline.config import (
26
+ MLFLOW_EXPERIMENT,
27
+ MLFLOW_TRACKING_URI,
28
+ MODELS_DIR,
29
+ )
30
+ from mlops_pipeline.resources import MLflowResource
31
+
32
+ optuna.logging.set_verbosity(optuna.logging.WARNING)
33
+
34
+
35
+ def _eval_metrics(y_true: np.ndarray, y_prob: np.ndarray) -> dict[str, float]:
36
+ y_pred = (y_prob >= 0.5).astype(int)
37
+ return {
38
+ "roc_auc": float(roc_auc_score(y_true, y_prob)),
39
+ "avg_precision": float(average_precision_score(y_true, y_prob)),
40
+ "f1": float(f1_score(y_true, y_pred, zero_division=0)),
41
+ }
42
+
43
+
44
+ @asset(group_name="training", required_resource_keys={"mlflow"})
45
+ def baseline_rf_model(
46
+ context: AssetExecutionContext,
47
+ train_test_split_asset: dict[str, Any],
48
+ ) -> dict[str, Any]:
49
+ """Train a RandomForest baseline and log to MLflow."""
50
+ mlflow_res: MLflowResource = context.resources.mlflow
51
+ mlflow_res.set_experiment()
52
+
53
+ X_train = train_test_split_asset["X_train"]
54
+ y_train = train_test_split_asset["y_train"]
55
+ X_test = train_test_split_asset["X_test"]
56
+ y_test = train_test_split_asset["y_test"]
57
+
58
+ with mlflow.start_run(run_name="RandomForest-baseline") as run:
59
+ params = {
60
+ "n_estimators": 300,
61
+ "max_depth": 12,
62
+ "min_samples_leaf": 4,
63
+ "class_weight": "balanced",
64
+ "random_state": 42,
65
+ "n_jobs": -1,
66
+ }
67
+ mlflow.log_params(params)
68
+
69
+ rf = RandomForestClassifier(**params)
70
+ rf.fit(X_train, y_train)
71
+
72
+ y_prob = rf.predict_proba(X_test)[:, 1]
73
+ metrics = _eval_metrics(y_test, y_prob)
74
+ mlflow.log_metrics(metrics)
75
+
76
+ # Log model artifact
77
+ mlflow.sklearn.log_model(rf, artifact_path="model")
78
+
79
+ run_id = run.info.run_id
80
+ context.log.info(f"RF roc_auc={metrics['roc_auc']:.4f} avg_pr={metrics['avg_precision']:.4f}")
81
+
82
+ import joblib
83
+ MODELS_DIR.mkdir(parents=True, exist_ok=True)
84
+ joblib.dump(rf, MODELS_DIR / "rf_baseline.joblib")
85
+
86
+ context.add_output_metadata({"run_id": run_id, **metrics})
87
+ return {"model_name": "RandomForest", "run_id": run_id, "metrics": metrics}
88
+
89
+
90
+ @asset(group_name="training", required_resource_keys={"mlflow"})
91
+ def tuned_xgb_model(
92
+ context: AssetExecutionContext,
93
+ train_test_split_asset: dict[str, Any],
94
+ ) -> dict[str, Any]:
95
+ """XGBoost hyperparameter search via Optuna (30 trials), log best run to MLflow."""
96
+ mlflow_res: MLflowResource = context.resources.mlflow
97
+ mlflow_res.set_experiment()
98
+
99
+ X_train = train_test_split_asset["X_train"]
100
+ y_train = train_test_split_asset["y_train"]
101
+ X_test = train_test_split_asset["X_test"]
102
+ y_test = train_test_split_asset["y_test"]
103
+
104
+ scale_pos_weight = float((y_train == 0).sum()) / float((y_train == 1).sum())
105
+
106
+ def objective(trial: optuna.Trial) -> float:
107
+ params = {
108
+ "n_estimators": trial.suggest_int("n_estimators", 100, 500),
109
+ "max_depth": trial.suggest_int("max_depth", 3, 10),
110
+ "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.3, log=True),
111
+ "subsample": trial.suggest_float("subsample", 0.6, 1.0),
112
+ "colsample_bytree": trial.suggest_float("colsample_bytree", 0.5, 1.0),
113
+ "min_child_weight": trial.suggest_int("min_child_weight", 1, 10),
114
+ "gamma": trial.suggest_float("gamma", 0.0, 5.0),
115
+ "reg_alpha": trial.suggest_float("reg_alpha", 1e-8, 1.0, log=True),
116
+ "reg_lambda": trial.suggest_float("reg_lambda", 1e-8, 1.0, log=True),
117
+ "scale_pos_weight": scale_pos_weight,
118
+ "eval_metric": "aucpr",
119
+ "random_state": 42,
120
+ "n_jobs": -1,
121
+ "use_label_encoder": False,
122
+ }
123
+ model = xgb.XGBClassifier(**params)
124
+ model.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=False)
125
+ y_prob = model.predict_proba(X_test)[:, 1]
126
+ return average_precision_score(y_test, y_prob)
127
+
128
+ study = optuna.create_study(direction="maximize", sampler=optuna.samplers.TPESampler(seed=42))
129
+ study.optimize(objective, n_trials=30, show_progress_bar=False)
130
+ context.log.info(f"Optuna best avg_pr={study.best_value:.4f} in {len(study.trials)} trials")
131
+
132
+ # Retrain best params on full train set and log to MLflow
133
+ best_params = {
134
+ **study.best_params,
135
+ "scale_pos_weight": scale_pos_weight,
136
+ "eval_metric": "aucpr",
137
+ "random_state": 42,
138
+ "n_jobs": -1,
139
+ "use_label_encoder": False,
140
+ }
141
+
142
+ with mlflow.start_run(run_name="XGBoost-Optuna") as run:
143
+ mlflow.log_params(best_params)
144
+ mlflow.log_param("optuna_n_trials", len(study.trials))
145
+
146
+ xgb_model = xgb.XGBClassifier(**best_params)
147
+ xgb_model.fit(X_train, y_train)
148
+
149
+ y_prob = xgb_model.predict_proba(X_test)[:, 1]
150
+ metrics = _eval_metrics(y_test, y_prob)
151
+ mlflow.log_metrics(metrics)
152
+
153
+ mlflow.xgboost.log_model(xgb_model, artifact_path="model")
154
+
155
+ # Log Optuna study summary
156
+ trials_df = study.trials_dataframe()
157
+ trials_path = MODELS_DIR / "optuna_trials.csv"
158
+ MODELS_DIR.mkdir(parents=True, exist_ok=True)
159
+ trials_df.to_csv(trials_path, index=False)
160
+ mlflow.log_artifact(str(trials_path), artifact_path="optuna")
161
+
162
+ run_id = run.info.run_id
163
+ context.log.info(f"XGB roc_auc={metrics['roc_auc']:.4f} avg_pr={metrics['avg_precision']:.4f}")
164
+
165
+ import joblib
166
+ joblib.dump(xgb_model, MODELS_DIR / "xgb_tuned.joblib")
167
+
168
+ context.add_output_metadata({"run_id": run_id, **metrics})
169
+ return {"model_name": "XGBoost", "run_id": run_id, "metrics": metrics}
170
+
171
+
172
+ @asset(group_name="training", required_resource_keys={"mlflow"})
173
+ def best_model_info(
174
+ context: AssetExecutionContext,
175
+ baseline_rf_model: dict[str, Any],
176
+ tuned_xgb_model: dict[str, Any],
177
+ ) -> dict[str, Any]:
178
+ """Pick the best model by avg_precision and register it in MLflow Model Registry."""
179
+ from mlops_pipeline.config import MODEL_REGISTRY_NAME
180
+
181
+ candidates = [baseline_rf_model, tuned_xgb_model]
182
+ best = max(candidates, key=lambda m: m["metrics"]["avg_precision"])
183
+
184
+ context.log.info(
185
+ f"Winner: {best['model_name']} "
186
+ f"avg_pr={best['metrics']['avg_precision']:.4f} "
187
+ f"roc_auc={best['metrics']['roc_auc']:.4f}"
188
+ )
189
+
190
+ # Register in MLflow Model Registry
191
+ mlflow_res: MLflowResource = context.resources.mlflow
192
+ client = mlflow_res.get_client()
193
+
194
+ model_uri = f"runs:/{best['run_id']}/model"
195
+ try:
196
+ client.create_registered_model(MODEL_REGISTRY_NAME)
197
+ except Exception:
198
+ pass # already exists
199
+
200
+ mv = client.create_model_version(
201
+ name=MODEL_REGISTRY_NAME,
202
+ source=model_uri,
203
+ run_id=best["run_id"],
204
+ )
205
+ client.set_registered_model_alias(
206
+ name=MODEL_REGISTRY_NAME,
207
+ alias="champion",
208
+ version=mv.version,
209
+ )
210
+ context.log.info(f"Registered {MODEL_REGISTRY_NAME} v{mv.version} as @champion")
211
+
212
+ result = {
213
+ "winner": best["model_name"],
214
+ "run_id": best["run_id"],
215
+ "registry_version": mv.version,
216
+ "metrics": best["metrics"],
217
+ "all_metrics": {m["model_name"]: m["metrics"] for m in candidates},
218
+ }
219
+ context.add_output_metadata(
220
+ {
221
+ "winner": best["model_name"],
222
+ "avg_precision": best["metrics"]["avg_precision"],
223
+ "roc_auc": best["metrics"]["roc_auc"],
224
+ "registry_version": mv.version,
225
+ }
226
+ )
227
+
228
+ # Write summary JSON for serving
229
+ MODELS_DIR.mkdir(parents=True, exist_ok=True)
230
+ (MODELS_DIR / "best_model_info.json").write_text(json.dumps(result, indent=2))
231
+
232
+ return result
src/mlops_pipeline/config.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Central configuration: paths, column names, model registry settings."""
2
+ from pathlib import Path
3
+
4
+ ROOT = Path(__file__).parent.parent.parent # project root: src/mlops_pipeline/ β†’ src/ β†’ root
5
+
6
+ # ── Data ──────────────────────────────────────────────────────────────────────
7
+ DATA_RAW_DIR = ROOT / "data" / "raw"
8
+ ORIG_CSV = ROOT / "Computer_Durability.csv"
9
+ PLUS_CSV = ROOT / "Computer_Durability_Plus.csv"
10
+
11
+ FEATURE_COLS = [
12
+ "Hours Used Per Day",
13
+ "Cost",
14
+ "User Age",
15
+ "Primary Usage",
16
+ "Brand",
17
+ "Computer Age (Months)",
18
+ ]
19
+ TARGET_COL = "Needs Replacement"
20
+ CATEGORICAL_COLS = ["Primary Usage", "Brand"]
21
+ NUMERIC_COLS = [c for c in FEATURE_COLS if c not in CATEGORICAL_COLS]
22
+
23
+ # ── MLflow ────────────────────────────────────────────────────────────────────
24
+ MLFLOW_TRACKING_URI = f"file://{ROOT / 'mlruns'}"
25
+ MLFLOW_EXPERIMENT = "computer-durability"
26
+ MODEL_REGISTRY_NAME = "ComputerDurabilityClassifier"
27
+
28
+ # ── Outputs ───────────────────────────────────────────────────────────────────
29
+ REPORTS_DIR = ROOT / "reports"
30
+ MODELS_DIR = ROOT / "models"
src/mlops_pipeline/definitions.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dagster Definitions: wires all assets and resources together."""
2
+ from dagster import Definitions, load_assets_from_modules
3
+
4
+ from mlops_pipeline.assets import data_assets, evaluation_assets, training_assets
5
+ from mlops_pipeline.config import MLFLOW_EXPERIMENT, MLFLOW_TRACKING_URI
6
+ from mlops_pipeline.resources import MLflowResource
7
+
8
+ all_assets = load_assets_from_modules(
9
+ [data_assets, training_assets, evaluation_assets]
10
+ )
11
+
12
+ defs = Definitions(
13
+ assets=all_assets,
14
+ resources={
15
+ "mlflow": MLflowResource(
16
+ tracking_uri=MLFLOW_TRACKING_URI,
17
+ experiment_name=MLFLOW_EXPERIMENT,
18
+ ),
19
+ },
20
+ )
src/mlops_pipeline/resources.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dagster resources: MLflow client wrapper."""
2
+ from __future__ import annotations
3
+
4
+ import mlflow
5
+ from dagster import ConfigurableResource
6
+
7
+
8
+ class MLflowResource(ConfigurableResource):
9
+ tracking_uri: str
10
+ experiment_name: str
11
+
12
+ def get_client(self) -> mlflow.MlflowClient:
13
+ mlflow.set_tracking_uri(self.tracking_uri)
14
+ return mlflow.MlflowClient(tracking_uri=self.tracking_uri)
15
+
16
+ def set_experiment(self) -> str:
17
+ mlflow.set_tracking_uri(self.tracking_uri)
18
+ mlflow.set_experiment(self.experiment_name)
19
+ return mlflow.get_experiment_by_name(self.experiment_name).experiment_id
uv.lock ADDED
The diff for this file is too large to render. See raw diff