"""Standalone synthetic teaching example. Run locally with Python, not in the HTML."""
import json
import numpy as np
import pandas as pd
from scipy.special import expit

def clean(value):
    if isinstance(value, dict): return {str(k):clean(v) for k,v in value.items()}
    if isinstance(value,(list,tuple,np.ndarray)): return [clean(v) for v in value]
    if isinstance(value,(np.integer,)): return int(value)
    if isinstance(value,(float,np.floating)): return float(value) if np.isfinite(value) else None
    if isinstance(value,(np.bool_,)): return bool(value)
    return value

def print_result(result):
    print(json.dumps(clean(result),ensure_ascii=False,allow_nan=False))


def best_split(x, y, min_leaf=1):
    """One-feature squared-error stump. None means no positive valid gain."""
    x, y = np.asarray(x,float), np.asarray(y,float)
    if x.ndim != 1 or y.shape != x.shape or min_leaf < 1:
        raise ValueError("Expected matching 1D arrays and min_leaf>=1")
    if len(x)==0 or not np.isfinite(x).all() or not np.isfinite(y).all():
        raise ValueError("Expected nonempty finite data")
    order = np.argsort(x,kind="stable"); xs, ys = x[order], y[order]
    parent = np.sum((ys-ys.mean())**2)
    best = None; best_sse = parent
    for i in range(min_leaf, len(x)-min_leaf+1):
        if xs[i-1] == xs[i]:
            continue
        left, right = ys[:i].mean(), ys[i:].mean()
        sse = np.sum((ys[:i]-left)**2)+np.sum((ys[i:]-right)**2)
        if sse < best_sse-1e-12:
            best_sse = sse
            best = (float((xs[i-1]+xs[i])/2), float(left), float(right), float(sse))
    return best

def fit_boosting(x, y, rounds=10, rate=.2, min_leaf=1):
    """Square-loss additive stumps. Return initial value, trees, train path."""
    if rounds < 0 or rate < 0 or not np.isfinite(rate):
        raise ValueError("Invalid rounds or learning rate")
    x, y = np.asarray(x,float), np.asarray(y,float)
    initial = float(y.mean()); prediction = np.full(len(y),initial)
    trees = []; history = [prediction.copy()]
    for _ in range(rounds):
        residual = y-prediction
        split = best_split(x,residual,min_leaf)
        if split is None:
            tree = (None,float(residual.mean()),float(residual.mean()))
            update = np.full(len(y),residual.mean())
        else:
            threshold,left,right,_ = split
            tree = (threshold,left,right)
            update = np.where(x<=threshold,left,right)
        prediction += rate*update
        trees.append(tree); history.append(prediction.copy())
    return initial, trees, np.asarray(history)

def predict_boosting(x, initial, trees, rate):
    prediction = np.full(len(x),initial,dtype=float)
    for threshold,left,right in trees:
        prediction += rate*(left if threshold is None else np.where(x<=threshold,left,right))
    return prediction

if __name__ == "__main__":
    x=np.arange(1,9,dtype=float); y=np.array([0.,0.,1.,2.,5.,5.,6.,9.])
    initial,trees,history=fit_boosting(x,y,rounds=12,rate=.2)
    final=predict_boosting(x,initial,trees,.2)
    np.testing.assert_allclose(final,history[-1])
    mse=np.mean((history-y)**2,axis=1)
    assert np.all(np.diff(mse)<=1e-10)
    constant,_,path=fit_boosting(np.ones(3),np.array([2.,3.,4.]),rounds=2,rate=.2)
    print_result({"initial":initial,"first_tree":trees[0],"mse_by_round":mse,
        "final_prediction":final,"constant_feature_prediction":path[-1]})
