| <!DOCTYPE html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| <title>Decision Tree Regression Game</title>
|
| <style>
|
| * {
|
| margin: 0;
|
| padding: 0;
|
| box-sizing: border-box;
|
| }
|
|
|
| body {
|
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| min-height: 100vh;
|
| padding: 20px;
|
| color: #333;
|
| }
|
|
|
| .container {
|
| max-width: 1200px;
|
| margin: 0 auto;
|
| background: rgba(255, 255, 255, 0.95);
|
| border-radius: 20px;
|
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
| overflow: hidden;
|
| }
|
|
|
| .header {
|
| background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
| padding: 30px;
|
| text-align: center;
|
| color: white;
|
| }
|
|
|
| .header h1 {
|
| font-size: 2.5rem;
|
| margin-bottom: 10px;
|
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
| }
|
|
|
| .header p {
|
| font-size: 1.1rem;
|
| opacity: 0.9;
|
| }
|
|
|
| .game-container {
|
| display: grid;
|
| grid-template-columns: 1fr 1fr;
|
| gap: 30px;
|
| padding: 30px;
|
| }
|
|
|
| @media (max-width: 768px) {
|
| .game-container {
|
| grid-template-columns: 1fr;
|
| }
|
| }
|
|
|
| .canvas-section {
|
| position: relative;
|
| }
|
|
|
| #regressionCanvas {
|
| width: 100%;
|
| height: 400px;
|
| border: 3px solid #4facfe;
|
| border-radius: 15px;
|
| background: #f8f9fa;
|
| cursor: crosshair;
|
| box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
| }
|
|
|
| .controls {
|
| background: #f8f9fa;
|
| padding: 25px;
|
| border-radius: 15px;
|
| box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
| }
|
|
|
| .control-group {
|
| margin-bottom: 20px;
|
| }
|
|
|
| .control-group h3 {
|
| color: #4facfe;
|
| margin-bottom: 15px;
|
| font-size: 1.2rem;
|
| }
|
|
|
| .btn {
|
| background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
| color: white;
|
| border: none;
|
| padding: 12px 24px;
|
| border-radius: 8px;
|
| cursor: pointer;
|
| font-size: 1rem;
|
| font-weight: 600;
|
| margin: 5px;
|
| transition: transform 0.2s, box-shadow 0.2s;
|
| }
|
|
|
| .btn:hover {
|
| transform: translateY(-2px);
|
| box-shadow: 0 8px 16px rgba(79, 172, 254, 0.3);
|
| }
|
|
|
| .btn-secondary {
|
| background: linear-gradient(135deg, #fd746c 0%, #ff9068 100%);
|
| }
|
|
|
| .btn-success {
|
| background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
| }
|
|
|
| .data-points {
|
| background: white;
|
| padding: 15px;
|
| border-radius: 10px;
|
| border: 2px solid #e9ecef;
|
| max-height: 200px;
|
| overflow-y: auto;
|
| }
|
|
|
| .data-point {
|
| display: flex;
|
| justify-content: space-between;
|
| padding: 8px;
|
| border-bottom: 1px solid #e9ecef;
|
| }
|
|
|
| .data-point:last-child {
|
| border-bottom: none;
|
| }
|
|
|
| .stats {
|
| display: grid;
|
| grid-template-columns: 1fr 1fr;
|
| gap: 15px;
|
| margin-top: 20px;
|
| }
|
|
|
| .stat-box {
|
| background: white;
|
| padding: 15px;
|
| border-radius: 10px;
|
| border: 2px solid #e9ecef;
|
| text-align: center;
|
| }
|
|
|
| .stat-value {
|
| font-size: 1.5rem;
|
| font-weight: bold;
|
| color: #4facfe;
|
| }
|
|
|
| .instructions {
|
| background: linear-gradient(135deg, #fffcdc 0%, #d9a7c7 100%);
|
| padding: 25px;
|
| border-radius: 15px;
|
| margin-top: 30px;
|
| }
|
|
|
| .instructions h3 {
|
| color: #764ba2;
|
| margin-bottom: 15px;
|
| }
|
|
|
| .instructions ul {
|
| list-style: none;
|
| padding: 0;
|
| }
|
|
|
| .instructions li {
|
| padding: 8px 0;
|
| border-bottom: 1px solid rgba(118, 75, 162, 0.2);
|
| }
|
|
|
| .instructions li:last-child {
|
| border-bottom: none;
|
| }
|
|
|
| .prediction-area {
|
| background: linear-gradient(135deg, #a8ff78 0%, #78ffd6 100%);
|
| padding: 20px;
|
| border-radius: 15px;
|
| margin-top: 20px;
|
| text-align: center;
|
| }
|
|
|
| .prediction-input {
|
| padding: 12px;
|
| border: 2px solid #4facfe;
|
| border-radius: 8px;
|
| font-size: 1rem;
|
| margin-right: 10px;
|
| width: 100px;
|
| }
|
|
|
| .tree-depth-slider {
|
| width: 100%;
|
| margin: 15px 0;
|
| }
|
|
|
| .slider-value {
|
| text-align: center;
|
| font-weight: bold;
|
| color: #4facfe;
|
| }
|
| </style>
|
| </head>
|
| <body>
|
| <div class="container">
|
| <div class="header">
|
| <h1>🎯 Decision Tree Regression Game</h1>
|
| <p>Visualize and interact with machine learning regression in real-time!</p>
|
| </div>
|
|
|
| <div class="game-container">
|
| <div class="canvas-section">
|
| <canvas id="regressionCanvas" width="500" height="400"></canvas>
|
| <div class="stats">
|
| <div class="stat-box">
|
| <div>Data Points</div>
|
| <div class="stat-value" id="pointCount">0</div>
|
| </div>
|
| <div class="stat-box">
|
| <div>Tree Depth</div>
|
| <div class="stat-value" id="treeDepth">1</div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div class="controls">
|
| <div class="control-group">
|
| <h3>🎮 Game Controls</h3>
|
| <button class="btn" onclick="addRandomPoints(10)">➕ Add 10 Random Points</button>
|
| <button class="btn btn-secondary" onclick="clearAllPoints()">🗑️ Clear All Points</button>
|
| <button class="btn btn-success" onclick="trainModel()">🚀 Train Model</button>
|
| </div>
|
|
|
| <div class="control-group">
|
| <h3>🌳 Tree Settings</h3>
|
| <label>Max Tree Depth:</label>
|
| <input type="range" min="1" max="10" value="3" class="tree-depth-slider" id="treeDepthSlider" oninput="updateTreeDepth()">
|
| <div class="slider-value" id="depthValue">3</div>
|
| </div>
|
|
|
| <div class="control-group">
|
| <h3>📊 Data Points</h3>
|
| <div class="data-points" id="dataPointsList">
|
| <div class="data-point">No data points yet...</div>
|
| </div>
|
| </div>
|
|
|
| <div class="prediction-area">
|
| <h3>🔮 Make a Prediction</h3>
|
| <input type="number" class="prediction-input" id="predictInput" placeholder="Enter X value" step="0.1">
|
| <button class="btn" onclick="makePrediction()">Predict Y</button>
|
| <div id="predictionResult" style="margin-top: 10px; font-weight: bold;"></div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div class="instructions">
|
|
|
| <p>You can see by incres depth it more non-linaer and by decring depth it more linaer </p>
|
| <h3>📖 How to Play</h3>
|
| <ul>
|
| <li>🎯 <strong>Click</strong> on the canvas to add data points</li>
|
| <li>🔄 Use the slider to adjust tree depth (complexity)</li>
|
| <li>🚀 Click "Train Model" to build the decision tree</li>
|
| <li>🔮 Enter an X value to predict the corresponding Y</li>
|
| <li>📊 Watch how the tree partitions the feature space</li>
|
| <li>🎨 Different colors represent different tree nodes</li>
|
| </ul>
|
| </div>
|
| </div>
|
|
|
| <script>
|
|
|
| const canvas = document.getElementById('regressionCanvas');
|
| const ctx = canvas.getContext('2d');
|
| const width = canvas.width;
|
| const height = canvas.height;
|
|
|
|
|
| let dataPoints = [];
|
| let decisionTree = null;
|
| let maxDepth = 3;
|
|
|
|
|
| function initCanvas() {
|
| ctx.clearRect(0, 0, width, height);
|
| drawGrid();
|
| drawAxes();
|
| }
|
|
|
|
|
| function drawGrid() {
|
| ctx.strokeStyle = '#e0e0e0';
|
| ctx.lineWidth = 1;
|
|
|
|
|
| for (let x = 0; x <= width; x += 50) {
|
| ctx.beginPath();
|
| ctx.moveTo(x, 0);
|
| ctx.lineTo(x, height);
|
| ctx.stroke();
|
| }
|
|
|
|
|
| for (let y = 0; y <= height; y += 50) {
|
| ctx.beginPath();
|
| ctx.moveTo(0, y);
|
| ctx.lineTo(width, y);
|
| ctx.stroke();
|
| }
|
| }
|
|
|
|
|
| function drawAxes() {
|
| ctx.strokeStyle = '#333';
|
| ctx.lineWidth = 2;
|
|
|
|
|
| ctx.beginPath();
|
| ctx.moveTo(0, height / 2);
|
| ctx.lineTo(width, height / 2);
|
| ctx.stroke();
|
|
|
|
|
| ctx.beginPath();
|
| ctx.moveTo(width / 2, 0);
|
| ctx.lineTo(width / 2, height);
|
| ctx.stroke();
|
|
|
|
|
| ctx.fillStyle = '#333';
|
| ctx.font = '12px Arial';
|
| ctx.fillText('X', width - 20, height / 2 - 10);
|
| ctx.fillText('Y', width / 2 + 10, 20);
|
| }
|
|
|
|
|
| function canvasToData(x, y) {
|
| return {
|
| x: (x - width / 2) / (width / 20),
|
| y: (height / 2 - y) / (height / 20)
|
| };
|
| }
|
|
|
|
|
| function dataToCanvas(x, y) {
|
| return {
|
| x: width / 2 + x * (width / 20),
|
| y: height / 2 - y * (height / 20)
|
| };
|
| }
|
|
|
|
|
| canvas.addEventListener('click', (event) => {
|
| const rect = canvas.getBoundingClientRect();
|
| const x = event.clientX - rect.left;
|
| const y = event.clientY - rect.top;
|
|
|
| const dataCoord = canvasToData(x, y);
|
| dataPoints.push(dataCoord);
|
|
|
| updateDataPointsList();
|
| drawDataPoints();
|
|
|
| if (decisionTree) {
|
| drawDecisionTree();
|
| }
|
| });
|
|
|
|
|
| function drawDataPoints() {
|
| dataPoints.forEach(point => {
|
| const canvasCoord = dataToCanvas(point.x, point.y);
|
|
|
| ctx.fillStyle = '#ff6b6b';
|
| ctx.beginPath();
|
| ctx.arc(canvasCoord.x, canvasCoord.y, 6, 0, 2 * Math.PI);
|
| ctx.fill();
|
|
|
| ctx.strokeStyle = '#fff';
|
| ctx.lineWidth = 2;
|
| ctx.stroke();
|
| });
|
|
|
| document.getElementById('pointCount').textContent = dataPoints.length;
|
| }
|
|
|
|
|
| function updateDataPointsList() {
|
| const list = document.getElementById('dataPointsList');
|
| list.innerHTML = '';
|
|
|
| if (dataPoints.length === 0) {
|
| list.innerHTML = '<div class="data-point">No data points yet...</div>';
|
| return;
|
| }
|
|
|
| dataPoints.forEach((point, index) => {
|
| const div = document.createElement('div');
|
| div.className = 'data-point';
|
| div.innerHTML = `
|
| <span>Point ${index + 1}</span>
|
| <span>(${point.x.toFixed(2)}, ${point.y.toFixed(2)})</span>
|
| `;
|
| list.appendChild(div);
|
| });
|
| }
|
|
|
|
|
| function addRandomPoints(count) {
|
| for (let i = 0; i < count; i++) {
|
| const x = (Math.random() - 0.5) * 18;
|
| const y = Math.sin(x * 2) * 3 + (Math.random() - 0.5) * 2;
|
| dataPoints.push({ x, y });
|
| }
|
|
|
| updateDataPointsList();
|
| drawDataPoints();
|
|
|
| if (decisionTree) {
|
| drawDecisionTree();
|
| }
|
| }
|
|
|
|
|
| function clearAllPoints() {
|
| dataPoints = [];
|
| decisionTree = null;
|
| updateDataPointsList();
|
| initCanvas();
|
| document.getElementById('predictionResult').textContent = '';
|
| }
|
|
|
|
|
| function updateTreeDepth() {
|
| maxDepth = parseInt(document.getElementById('treeDepthSlider').value);
|
| document.getElementById('depthValue').textContent = maxDepth;
|
| document.getElementById('treeDepth').textContent = maxDepth;
|
|
|
| if (decisionTree) {
|
| trainModel();
|
| }
|
| }
|
|
|
|
|
| class TreeNode {
|
| constructor(featureIndex = null, threshold = null, value = null, left = null, right = null) {
|
| this.featureIndex = featureIndex;
|
| this.threshold = threshold;
|
| this.value = value;
|
| this.left = left;
|
| this.right = right;
|
| }
|
| }
|
|
|
|
|
| function trainModel() {
|
| if (dataPoints.length < 2) {
|
| alert('Need at least 2 data points to train the model!');
|
| return;
|
| }
|
|
|
|
|
| const X = dataPoints.map(p => [p.x]);
|
| const y = dataPoints.map(p => p.y);
|
|
|
|
|
| decisionTree = buildDecisionTree(X, y, maxDepth);
|
|
|
| drawDecisionTree();
|
| }
|
|
|
|
|
| function buildDecisionTree(X, y, maxDepth, depth = 0) {
|
| if (depth >= maxDepth || X.length <= 1) {
|
| return new TreeNode(null, null, mean(y));
|
| }
|
|
|
|
|
| const { bestFeature, bestThreshold, bestScore } = findBestSplit(X, y);
|
|
|
| if (bestScore === -Infinity) {
|
| return new TreeNode(null, null, mean(y));
|
| }
|
|
|
|
|
| const [leftX, leftY, rightX, rightY] = splitData(X, y, bestFeature, bestThreshold);
|
|
|
|
|
| const left = buildDecisionTree(leftX, leftY, maxDepth, depth + 1);
|
| const right = buildDecisionTree(rightX, rightY, maxDepth, depth + 1);
|
|
|
| return new TreeNode(bestFeature, bestThreshold, null, left, right);
|
| }
|
|
|
|
|
| function findBestSplit(X, y) {
|
| let bestFeature = 0;
|
| let bestThreshold = 0;
|
| let bestScore = -Infinity;
|
|
|
| for (let feature = 0; feature < X[0].length; feature++) {
|
| const featureValues = X.map(x => x[feature]);
|
| const uniqueValues = [...new Set(featureValues)].sort((a, b) => a - b);
|
|
|
| for (let i = 0; i < uniqueValues.length - 1; i++) {
|
| const threshold = (uniqueValues[i] + uniqueValues[i + 1]) / 2;
|
| const score = calculateSplitScore(X, y, feature, threshold);
|
|
|
| if (score > bestScore) {
|
| bestScore = score;
|
| bestFeature = feature;
|
| bestThreshold = threshold;
|
| }
|
| }
|
| }
|
|
|
| return { bestFeature, bestThreshold, bestScore };
|
| }
|
|
|
|
|
| function calculateSplitScore(X, y, feature, threshold) {
|
| const [leftX, leftY, rightX, rightY] = splitData(X, y, feature, threshold);
|
|
|
| if (leftY.length === 0 || rightY.length === 0) {
|
| return -Infinity;
|
| }
|
|
|
| const totalVariance = variance(y);
|
| const leftVariance = variance(leftY);
|
| const rightVariance = variance(rightY);
|
|
|
| const leftWeight = leftY.length / y.length;
|
| const rightWeight = rightY.length / y.length;
|
|
|
| return totalVariance - (leftWeight * leftVariance + rightWeight * rightVariance);
|
| }
|
|
|
|
|
| function splitData(X, y, feature, threshold) {
|
| const leftX = [];
|
| const leftY = [];
|
| const rightX = [];
|
| const rightY = [];
|
|
|
| for (let i = 0; i < X.length; i++) {
|
| if (X[i][feature] <= threshold) {
|
| leftX.push(X[i]);
|
| leftY.push(y[i]);
|
| } else {
|
| rightX.push(X[i]);
|
| rightY.push(y[i]);
|
| }
|
| }
|
|
|
| return [leftX, leftY, rightX, rightY];
|
| }
|
|
|
|
|
| function mean(arr) {
|
| return arr.reduce((sum, val) => sum + val, 0) / arr.length;
|
| }
|
|
|
|
|
| function variance(arr) {
|
| const avg = mean(arr);
|
| return arr.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / arr.length;
|
| }
|
|
|
|
|
| function predict(tree, x) {
|
| if (tree.value !== null) {
|
| return tree.value;
|
| }
|
|
|
| if (x[tree.featureIndex] <= tree.threshold) {
|
| return predict(tree.left, x);
|
| } else {
|
| return predict(tree.right, x);
|
| }
|
| }
|
|
|
|
|
| function drawDecisionTree() {
|
| initCanvas();
|
| drawDataPoints();
|
|
|
| if (!decisionTree) return;
|
|
|
|
|
| const colors = ['#4facfe', '#fd746c', '#56ab2f', '#a8ff78', '#ff9068', '#00f2fe', '#ff6b6b'];
|
|
|
|
|
| ctx.lineWidth = 3;
|
|
|
| for (let x = -10; x <= 10; x += 0.1) {
|
| const prediction = predict(decisionTree, [x]);
|
| const canvasCoord = dataToCanvas(x, prediction);
|
| const nextX = x + 0.1;
|
| const nextPrediction = predict(decisionTree, [nextX]);
|
| const nextCanvasCoord = dataToCanvas(nextX, nextPrediction);
|
|
|
|
|
| const colorIndex = Math.abs(Math.floor(x * 2)) % colors.length;
|
| ctx.strokeStyle = colors[colorIndex];
|
|
|
| ctx.beginPath();
|
| ctx.moveTo(canvasCoord.x, canvasCoord.y);
|
| ctx.lineTo(nextCanvasCoord.x, nextCanvasCoord.y);
|
| ctx.stroke();
|
| }
|
| }
|
|
|
|
|
| function makePrediction() {
|
| const input = document.getElementById('predictInput');
|
| const xValue = parseFloat(input.value);
|
|
|
| if (isNaN(xValue)) {
|
| alert('Please enter a valid number!');
|
| return;
|
| }
|
|
|
| if (!decisionTree) {
|
| alert('Please train the model first!');
|
| return;
|
| }
|
|
|
| const prediction = predict(decisionTree, [xValue]);
|
| document.getElementById('predictionResult').textContent =
|
| `Prediction: For x = ${xValue.toFixed(2)}, y ≈ ${prediction.toFixed(2)}`;
|
|
|
|
|
| const canvasCoord = dataToCanvas(xValue, prediction);
|
|
|
| ctx.fillStyle = '#ff00ff';
|
| ctx.beginPath();
|
| ctx.arc(canvasCoord.x, canvasCoord.y, 8, 0, 2 * Math.PI);
|
| ctx.fill();
|
|
|
| ctx.strokeStyle = '#fff';
|
| ctx.lineWidth = 2;
|
| ctx.stroke();
|
| }
|
|
|
|
|
| window.onload = function() {
|
| initCanvas();
|
| updateTreeDepth();
|
| };
|
| </script>
|
|
|
| <style>
|
| .my-btn {
|
| background: linear-gradient(135deg, #4facfe, #00f2fe);
|
| color: white;
|
| padding: 12px 24px;
|
| border: none;
|
| border-radius: 8px;
|
| margin-left: 550px;
|
| margin-right: 550px;
|
| display: flex;
|
| justify-content: center;
|
| align-items: center;
|
| font-size: 16px;
|
| font-weight: bold;
|
| cursor: pointer;
|
| align-items: center;
|
| transition: all 0.3s ease;
|
| box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
| }
|
|
|
| .my-btn:hover {
|
| background: linear-gradient(135deg, #43e97b, #38f9d7);
|
| transform: scale(1.05);
|
| box-shadow: 0 6px 12px rgba(0,0,0,0.3);
|
| }
|
|
|
| .my-btn:active {
|
| transform: scale(0.98);
|
| }
|
| </style>
|
|
|
| <a href="/dtr" class="my-btn">Go back to decision tree regrssion</a>
|
|
|
|
|
|
|
|
|
| </body>
|
| </html>
|
|
|
|
|