linesight 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- linesight/__init__.py +19 -0
- linesight/base.py +195 -0
- linesight/exceptions/__init__.py +13 -0
- linesight/exceptions/convergence_warning.py +9 -0
- linesight/exceptions/data_warning.py +6 -0
- linesight/exceptions/gradient_error.py +5 -0
- linesight/exceptions/not_fitted_error.py +13 -0
- linesight/exceptions/shape_error.py +18 -0
- linesight/metrics/__init__.py +5 -0
- linesight/metrics/accuracy.py +9 -0
- linesight/metrics/mae.py +5 -0
- linesight/metrics/mse.py +5 -0
- linesight/metrics/r2.py +20 -0
- linesight/metrics/rmse.py +6 -0
- linesight/regression/__init__.py +6 -0
- linesight/regression/elasticnet/__init__.py +1 -0
- linesight/regression/elasticnet/core.py +103 -0
- linesight/regression/elasticnet/engine/__init__.py +0 -0
- linesight/regression/elasticnet/engine/compute_loss.py +8 -0
- linesight/regression/elasticnet/engine/fit.py +72 -0
- linesight/regression/elasticnet/engine/get_history.py +9 -0
- linesight/regression/elasticnet/engine/predict.py +9 -0
- linesight/regression/elasticnet/engine/score.py +13 -0
- linesight/regression/elasticnet/engine/soft_threshold.py +8 -0
- linesight/regression/elasticnet/explain/__init__.py +1 -0
- linesight/regression/elasticnet/explain/explain_coefficients.py +29 -0
- linesight/regression/elasticnet/explain/explain_regularization.py +33 -0
- linesight/regression/elasticnet/visualization/__init__.py +1 -0
- linesight/regression/elasticnet/visualization/animate_l1_ratio_sweep.py +102 -0
- linesight/regression/elasticnet/visualization/compare_regularization_methods.py +100 -0
- linesight/regression/elasticnet/visualization/plot_actual_vs_predicted.py +69 -0
- linesight/regression/elasticnet/visualization/plot_coefficient_shrinkage.py +43 -0
- linesight/regression/elasticnet/visualization/plot_fit.py +36 -0
- linesight/regression/elasticnet/visualization/plot_l1_l2_balance.py +113 -0
- linesight/regression/elasticnet/visualization/plot_learning_curve.py +127 -0
- linesight/regression/elasticnet/visualization/plot_loss_curve.py +27 -0
- linesight/regression/lasso/__init__.py +1 -0
- linesight/regression/lasso/core.py +105 -0
- linesight/regression/lasso/engine/__init__.py +0 -0
- linesight/regression/lasso/engine/compute_loss.py +7 -0
- linesight/regression/lasso/engine/fit.py +71 -0
- linesight/regression/lasso/engine/get_history.py +9 -0
- linesight/regression/lasso/engine/predict.py +9 -0
- linesight/regression/lasso/engine/score.py +13 -0
- linesight/regression/lasso/engine/soft_threshold.py +29 -0
- linesight/regression/lasso/explain/__init__.py +0 -0
- linesight/regression/lasso/explain/explain_coefficients.py +30 -0
- linesight/regression/lasso/explain/explain_regularization.py +30 -0
- linesight/regression/lasso/explain/explain_sparsity.py +36 -0
- linesight/regression/lasso/visualization/__init__.py +0 -0
- linesight/regression/lasso/visualization/animate_coordinate_descent.py +104 -0
- linesight/regression/lasso/visualization/compare_with_linear.py +49 -0
- linesight/regression/lasso/visualization/plot_actual_vs_predicted.py +69 -0
- linesight/regression/lasso/visualization/plot_coefficient_shrinkage.py +44 -0
- linesight/regression/lasso/visualization/plot_constraint_region.py +92 -0
- linesight/regression/lasso/visualization/plot_feature_elimination.py +88 -0
- linesight/regression/lasso/visualization/plot_fit.py +37 -0
- linesight/regression/lasso/visualization/plot_learning_curve.py +127 -0
- linesight/regression/lasso/visualization/plot_loss_curve.py +28 -0
- linesight/regression/lasso/visualization/plot_sparsity_path.py +126 -0
- linesight/regression/linear/__init__.py +1 -0
- linesight/regression/linear/core.py +134 -0
- linesight/regression/linear/engine/__init__.py +0 -0
- linesight/regression/linear/engine/compute_loss.py +23 -0
- linesight/regression/linear/engine/fit.py +59 -0
- linesight/regression/linear/engine/get_history.py +31 -0
- linesight/regression/linear/engine/gradient_step.py +42 -0
- linesight/regression/linear/engine/predict.py +25 -0
- linesight/regression/linear/engine/score.py +38 -0
- linesight/regression/linear/explain/__init__.py +0 -0
- linesight/regression/linear/explain/explain_coefficients.py +39 -0
- linesight/regression/linear/explain/explain_fit.py +66 -0
- linesight/regression/linear/explain/show_equation.py +25 -0
- linesight/regression/linear/visualization/__init__.py +0 -0
- linesight/regression/linear/visualization/animate_loss_surface_path.py +157 -0
- linesight/regression/linear/visualization/animate_training.py +109 -0
- linesight/regression/linear/visualization/compare_learning_rates.py +97 -0
- linesight/regression/linear/visualization/plot_actual_vs_predicted.py +69 -0
- linesight/regression/linear/visualization/plot_fit.py +103 -0
- linesight/regression/linear/visualization/plot_gradient_vectors.py +145 -0
- linesight/regression/linear/visualization/plot_learning_curve.py +126 -0
- linesight/regression/linear/visualization/plot_loss_curve.py +78 -0
- linesight/regression/linear/visualization/plot_loss_surface.py +135 -0
- linesight/regression/linear/visualization/plot_prediction_intervals.py +190 -0
- linesight/regression/linear/visualization/plot_residuals.py +74 -0
- linesight/regression/linear/visualization/plot_sensitivity_analysis.py +195 -0
- linesight/regression/logistic/__init__.py +1 -0
- linesight/regression/logistic/core.py +114 -0
- linesight/regression/logistic/engine/__init__.py +1 -0
- linesight/regression/logistic/engine/compute_loss.py +8 -0
- linesight/regression/logistic/engine/fit.py +88 -0
- linesight/regression/logistic/engine/get_history.py +8 -0
- linesight/regression/logistic/engine/gradient_step.py +21 -0
- linesight/regression/logistic/engine/predict.py +9 -0
- linesight/regression/logistic/engine/predict_proba.py +12 -0
- linesight/regression/logistic/engine/score.py +12 -0
- linesight/regression/logistic/engine/sigmoid.py +32 -0
- linesight/regression/logistic/explain/__init__.py +1 -0
- linesight/regression/logistic/explain/explain_boundary.py +23 -0
- linesight/regression/logistic/explain/explain_coefficients.py +27 -0
- linesight/regression/logistic/explain/explain_sigmoid.py +21 -0
- linesight/regression/logistic/visualization/__init__.py +1 -0
- linesight/regression/logistic/visualization/animate_boundary.py +87 -0
- linesight/regression/logistic/visualization/plot_actual_vs_predicted.py +69 -0
- linesight/regression/logistic/visualization/plot_calibration_curve.py +138 -0
- linesight/regression/logistic/visualization/plot_confusion_matrix.py +105 -0
- linesight/regression/logistic/visualization/plot_decision_boundary.py +106 -0
- linesight/regression/logistic/visualization/plot_learning_curve.py +126 -0
- linesight/regression/logistic/visualization/plot_log_odds.py +129 -0
- linesight/regression/logistic/visualization/plot_loss_curve.py +27 -0
- linesight/regression/logistic/visualization/plot_probability_surface.py +37 -0
- linesight/regression/logistic/visualization/plot_residuals.py +30 -0
- linesight/regression/logistic/visualization/plot_roc_curve.py +118 -0
- linesight/regression/logistic/visualization/plot_sigmoid.py +32 -0
- linesight/regression/logistic/visualization/plot_threshold_sensitivity.py +80 -0
- linesight/regression/multiple/__init__.py +1 -0
- linesight/regression/multiple/core.py +103 -0
- linesight/regression/multiple/engine/__init__.py +0 -0
- linesight/regression/multiple/engine/compute_loss.py +5 -0
- linesight/regression/multiple/engine/fit.py +87 -0
- linesight/regression/multiple/engine/get_history.py +13 -0
- linesight/regression/multiple/engine/gradient_step.py +23 -0
- linesight/regression/multiple/engine/predict.py +11 -0
- linesight/regression/multiple/engine/score.py +17 -0
- linesight/regression/multiple/explain/__init__.py +0 -0
- linesight/regression/multiple/explain/explain_coefficients.py +29 -0
- linesight/regression/multiple/explain/show_equation.py +14 -0
- linesight/regression/multiple/visualization/__init__.py +0 -0
- linesight/regression/multiple/visualization/plot_3d_loss_slice.py +113 -0
- linesight/regression/multiple/visualization/plot_actual_vs_predicted.py +69 -0
- linesight/regression/multiple/visualization/plot_correlation_matrix.py +35 -0
- linesight/regression/multiple/visualization/plot_feature_importance.py +59 -0
- linesight/regression/multiple/visualization/plot_fit.py +62 -0
- linesight/regression/multiple/visualization/plot_learning_curve.py +126 -0
- linesight/regression/multiple/visualization/plot_multicollinearity.py +180 -0
- linesight/regression/multiple/visualization/plot_partial_regression.py +67 -0
- linesight/regression/multiple/visualization/plot_prediction_plane.py +42 -0
- linesight/regression/multiple/visualization/plot_residual_heatmap.py +87 -0
- linesight/regression/polynomial/__init__.py +0 -0
- linesight/regression/polynomial/core.py +93 -0
- linesight/regression/polynomial/engine/__init__.py +0 -0
- linesight/regression/polynomial/engine/expand_features.py +30 -0
- linesight/regression/polynomial/engine/fit.py +117 -0
- linesight/regression/polynomial/engine/predict.py +14 -0
- linesight/regression/polynomial/explain/__init__.py +0 -0
- linesight/regression/polynomial/explain/show_equation.py +29 -0
- linesight/regression/polynomial/visualization/__init__.py +0 -0
- linesight/regression/polynomial/visualization/animate_degree_increase.py +88 -0
- linesight/regression/polynomial/visualization/compare_degrees.py +110 -0
- linesight/regression/polynomial/visualization/plot_actual_vs_predicted.py +69 -0
- linesight/regression/polynomial/visualization/plot_basis_functions.py +138 -0
- linesight/regression/polynomial/visualization/plot_fit.py +59 -0
- linesight/regression/polynomial/visualization/plot_learning_curve.py +126 -0
- linesight/regression/ridge/__init__.py +1 -0
- linesight/regression/ridge/core.py +109 -0
- linesight/regression/ridge/engine/__init__.py +0 -0
- linesight/regression/ridge/engine/compute_loss.py +7 -0
- linesight/regression/ridge/engine/fit.py +87 -0
- linesight/regression/ridge/engine/get_history.py +9 -0
- linesight/regression/ridge/engine/gradient_step.py +28 -0
- linesight/regression/ridge/engine/predict.py +11 -0
- linesight/regression/ridge/engine/score.py +13 -0
- linesight/regression/ridge/explain/__init__.py +0 -0
- linesight/regression/ridge/explain/explain_coefficients.py +32 -0
- linesight/regression/ridge/explain/explain_regularization.py +38 -0
- linesight/regression/ridge/visualization/__init__.py +0 -0
- linesight/regression/ridge/visualization/animate_regularization.py +100 -0
- linesight/regression/ridge/visualization/compare_with_linear.py +58 -0
- linesight/regression/ridge/visualization/plot_actual_vs_predicted.py +69 -0
- linesight/regression/ridge/visualization/plot_bias_variance_tradeoff.py +110 -0
- linesight/regression/ridge/visualization/plot_coefficient_shrinkage.py +75 -0
- linesight/regression/ridge/visualization/plot_constraint_region.py +90 -0
- linesight/regression/ridge/visualization/plot_effective_degrees_of_freedom.py +117 -0
- linesight/regression/ridge/visualization/plot_fit.py +38 -0
- linesight/regression/ridge/visualization/plot_learning_curve.py +126 -0
- linesight/regression/ridge/visualization/plot_loss_curve.py +30 -0
- linesight/utils/__init__.py +13 -0
- linesight/utils/array_utils.py +23 -0
- linesight/utils/colors.py +43 -0
- linesight/utils/environment.py +28 -0
- linesight/utils/history.py +16 -0
- linesight/utils/validators.py +70 -0
- linesight/utils/viz_context.py +51 -0
- linesight-0.1.0.dist-info/METADATA +160 -0
- linesight-0.1.0.dist-info/RECORD +188 -0
- linesight-0.1.0.dist-info/WHEEL +5 -0
- linesight-0.1.0.dist-info/licenses/LICENSE +21 -0
- linesight-0.1.0.dist-info/top_level.txt +1 -0
linesight/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from linesight.regression.linear.core import LinearRegression
|
|
2
|
+
from linesight.regression.polynomial.core import PolynomialRegression
|
|
3
|
+
from linesight.regression.multiple.core import MultipleLinearRegression
|
|
4
|
+
from linesight.regression.ridge.core import RidgeRegression
|
|
5
|
+
from linesight.regression.lasso.core import LassoRegression
|
|
6
|
+
from linesight.regression.elasticnet.core import ElasticNetRegression
|
|
7
|
+
from linesight.regression.logistic.core import LogisticRegression
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"LinearRegression",
|
|
13
|
+
"PolynomialRegression",
|
|
14
|
+
"MultipleLinearRegression",
|
|
15
|
+
"RidgeRegression",
|
|
16
|
+
"LassoRegression",
|
|
17
|
+
"ElasticNetRegression",
|
|
18
|
+
"LogisticRegression",
|
|
19
|
+
]
|
linesight/base.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
from linesight.exceptions import LineSightNotFittedError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LineSightBase:
|
|
6
|
+
|
|
7
|
+
_is_fitted: bool = False
|
|
8
|
+
|
|
9
|
+
def _check_fitted(self, method_name: str) -> None:
|
|
10
|
+
if not self._is_fitted:
|
|
11
|
+
raise LineSightNotFittedError(
|
|
12
|
+
f"Call fit(X, y) before calling {method_name}()."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def _validate_hyperparams(self) -> None:
|
|
16
|
+
"""Hook for subclasses to validate hyperparams before fitting."""
|
|
17
|
+
if hasattr(self, 'learning_rate') and getattr(self, 'learning_rate') <= 0:
|
|
18
|
+
raise ValueError("learning_rate must be > 0")
|
|
19
|
+
if hasattr(self, 'epochs') and getattr(self, 'epochs') <= 0:
|
|
20
|
+
raise ValueError("epochs must be > 0")
|
|
21
|
+
if hasattr(self, 'alpha') and getattr(self, 'alpha') < 0:
|
|
22
|
+
raise ValueError("alpha must be >= 0")
|
|
23
|
+
if hasattr(self, 'l1_ratio') and not (0 <= getattr(self, 'l1_ratio') <= 1):
|
|
24
|
+
raise ValueError("l1_ratio must be between 0 and 1")
|
|
25
|
+
if hasattr(self, 'degree') and getattr(self, 'degree') < 1:
|
|
26
|
+
raise ValueError("degree must be >= 1")
|
|
27
|
+
if hasattr(self, 'batch_size'):
|
|
28
|
+
bs = getattr(self, 'batch_size')
|
|
29
|
+
if bs is not None and bs <= 0:
|
|
30
|
+
raise ValueError("batch_size must be >= 1 or None")
|
|
31
|
+
|
|
32
|
+
def show(self, animation_obj=None, fig=None):
|
|
33
|
+
"""
|
|
34
|
+
Display a figure or animation correctly for the current environment.
|
|
35
|
+
|
|
36
|
+
THE CORRECT BEHAVIOR — read carefully:
|
|
37
|
+
|
|
38
|
+
For STATIC figures (fig parameter):
|
|
39
|
+
- In Jupyter/Colab: plt.show() closes the figure and triggers
|
|
40
|
+
the inline backend to render it. Do NOT return fig after plt.show()
|
|
41
|
+
because the figure object is now closed. Return None.
|
|
42
|
+
- In script: plt.show() opens the window. Return None.
|
|
43
|
+
- NEVER call IPython.display.display(fig) — causes triple rendering.
|
|
44
|
+
|
|
45
|
+
For ANIMATIONS (animation_obj parameter):
|
|
46
|
+
- In Jupyter/Colab: return HTML(anim.to_jshtml()).
|
|
47
|
+
Do NOT call plt.show() first — it closes the figure.
|
|
48
|
+
- In script: plt.show() runs the animation in a window.
|
|
49
|
+
|
|
50
|
+
This corrects the bug in the original specification where static
|
|
51
|
+
figures returned fig after plt.show() had already closed them.
|
|
52
|
+
"""
|
|
53
|
+
from linesight.utils.environment import _detect_environment
|
|
54
|
+
env = _detect_environment()
|
|
55
|
+
|
|
56
|
+
if animation_obj is not None:
|
|
57
|
+
if env in ('jupyter', 'colab'):
|
|
58
|
+
try:
|
|
59
|
+
from IPython.display import HTML
|
|
60
|
+
plt.close() # close the underlying figure to free memory
|
|
61
|
+
return HTML(animation_obj.to_jshtml())
|
|
62
|
+
except ImportError:
|
|
63
|
+
plt.show()
|
|
64
|
+
return None
|
|
65
|
+
else:
|
|
66
|
+
plt.show()
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
elif fig is not None:
|
|
70
|
+
# For static figures: plt.show() handles both environments.
|
|
71
|
+
# In Jupyter with %matplotlib inline, plt.show() triggers the
|
|
72
|
+
# inline renderer. In scripts it opens the window.
|
|
73
|
+
# After plt.show(), the figure is closed. Return None always.
|
|
74
|
+
plt.tight_layout()
|
|
75
|
+
plt.show()
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
def save(self, filepath: str, fig=None, animation_obj=None,
|
|
81
|
+
dpi: int = 150, fps: int = 20) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Save a figure or animation to disk.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
filepath : str
|
|
88
|
+
Full path including extension.
|
|
89
|
+
For figures: use .png, .pdf, .svg
|
|
90
|
+
For animations: use .gif or .mp4
|
|
91
|
+
Example: "my_plot.png" or "training.gif"
|
|
92
|
+
fig : matplotlib.figure.Figure, optional
|
|
93
|
+
animation_obj : FuncAnimation, optional
|
|
94
|
+
dpi : int, default 150
|
|
95
|
+
Resolution for raster formats (png, gif).
|
|
96
|
+
fps : int, default 20
|
|
97
|
+
Frames per second for animation exports.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
str — the filepath that was saved to, for confirmation.
|
|
102
|
+
|
|
103
|
+
Usage in visualization methods
|
|
104
|
+
--------------------------------
|
|
105
|
+
To let users save without displaying:
|
|
106
|
+
fig = model.plot_fit(X, y, display=False)
|
|
107
|
+
model.save("fit.png", fig=fig)
|
|
108
|
+
|
|
109
|
+
Or as a one-liner (display=False returns the object):
|
|
110
|
+
model.save("training.gif",
|
|
111
|
+
animation_obj=model.animate_training(X, y, display=False))
|
|
112
|
+
|
|
113
|
+
Raises
|
|
114
|
+
------
|
|
115
|
+
ValueError if neither fig nor animation_obj is provided.
|
|
116
|
+
ImportError with helpful message if saving .mp4 requires ffmpeg.
|
|
117
|
+
"""
|
|
118
|
+
if fig is None and animation_obj is None:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
"Pass either fig= or animation_obj= to save().\n"
|
|
121
|
+
"Get the object by calling the visualization method "
|
|
122
|
+
"with display=False."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if fig is not None:
|
|
126
|
+
fig.savefig(filepath, dpi=dpi, bbox_inches='tight')
|
|
127
|
+
print(f"Saved to: {filepath}")
|
|
128
|
+
return filepath
|
|
129
|
+
|
|
130
|
+
if animation_obj is not None:
|
|
131
|
+
if filepath.endswith('.gif'):
|
|
132
|
+
animation_obj.save(filepath, writer='pillow', fps=fps)
|
|
133
|
+
elif filepath.endswith('.mp4'):
|
|
134
|
+
try:
|
|
135
|
+
animation_obj.save(filepath, writer='ffmpeg', fps=fps)
|
|
136
|
+
except Exception:
|
|
137
|
+
raise ImportError(
|
|
138
|
+
"Saving .mp4 requires ffmpeg installed on your system.\n"
|
|
139
|
+
"Install it with: conda install ffmpeg\n"
|
|
140
|
+
"Or save as .gif instead: model.save('training.gif', ...)"
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
animation_obj.save(filepath, fps=fps)
|
|
144
|
+
print(f"Saved to: {filepath}")
|
|
145
|
+
return filepath
|
|
146
|
+
|
|
147
|
+
def summary(self) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Print a complete model summary: parameters, fit quality, coefficients.
|
|
150
|
+
|
|
151
|
+
Every regression class overrides this to include model-specific info.
|
|
152
|
+
The base implementation raises NotFittedError if called before fit().
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
str — the summary text. Also printed (in script mode) or returned
|
|
157
|
+
(in Jupyter, displayed automatically as the cell's last value).
|
|
158
|
+
"""
|
|
159
|
+
self._check_fitted("summary")
|
|
160
|
+
raise NotImplementedError(
|
|
161
|
+
"summary() must be implemented by each regression subclass."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def refit(self, X, y):
|
|
165
|
+
"""
|
|
166
|
+
Re-train the model from scratch on new data.
|
|
167
|
+
|
|
168
|
+
Resets ALL state: coefficients, intercept, history, fitted flag.
|
|
169
|
+
Equivalent to creating a new instance with the same hyperparameters
|
|
170
|
+
and calling fit(X, y).
|
|
171
|
+
|
|
172
|
+
Why this exists
|
|
173
|
+
---------------
|
|
174
|
+
If a student calls fit() twice on the same model object, the second
|
|
175
|
+
call continues from where the first left off (coef_ is NOT reset to 0).
|
|
176
|
+
This produces confusing results. refit() makes the intention explicit
|
|
177
|
+
and resets properly.
|
|
178
|
+
|
|
179
|
+
Usage
|
|
180
|
+
-----
|
|
181
|
+
model.fit(X_train, y_train) # initial training
|
|
182
|
+
model.refit(X_new, y_new) # clean reset + retrain on new data
|
|
183
|
+
"""
|
|
184
|
+
# Reset state — subclasses that use theta_ instead of coef_/intercept_
|
|
185
|
+
# must override this to reset theta_ as well
|
|
186
|
+
if hasattr(self, 'coef_'):
|
|
187
|
+
self.coef_ = 0.0
|
|
188
|
+
if hasattr(self, 'intercept_'):
|
|
189
|
+
self.intercept_ = 0.0
|
|
190
|
+
if hasattr(self, 'theta_'):
|
|
191
|
+
self.theta_ = None
|
|
192
|
+
self._is_fitted = False
|
|
193
|
+
from linesight.utils.history import TrainingHistory
|
|
194
|
+
self._history = TrainingHistory(learning_rate=getattr(self, 'learning_rate', 0))
|
|
195
|
+
return self.fit(X, y)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from linesight.exceptions.shape_error import LineSightShapeError
|
|
2
|
+
from linesight.exceptions.not_fitted_error import LineSightNotFittedError
|
|
3
|
+
from linesight.exceptions.convergence_warning import LineSightConvergenceWarning
|
|
4
|
+
from linesight.exceptions.data_warning import LineSightDataWarning
|
|
5
|
+
from linesight.exceptions.gradient_error import LineSightGradientError
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"LineSightShapeError",
|
|
9
|
+
"LineSightNotFittedError",
|
|
10
|
+
"LineSightConvergenceWarning",
|
|
11
|
+
"LineSightDataWarning",
|
|
12
|
+
"LineSightGradientError",
|
|
13
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class LineSightNotFittedError(RuntimeError):
|
|
2
|
+
"""
|
|
3
|
+
Raised when any method that requires a trained model is called before fit().
|
|
4
|
+
|
|
5
|
+
Always constructed with the method name that was called.
|
|
6
|
+
|
|
7
|
+
Example
|
|
8
|
+
-------
|
|
9
|
+
raise LineSightNotFittedError(
|
|
10
|
+
"Call fit(X, y) before calling predict()."
|
|
11
|
+
)
|
|
12
|
+
"""
|
|
13
|
+
pass
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class LineSightShapeError(ValueError):
|
|
2
|
+
"""
|
|
3
|
+
Raised when array shapes are incompatible.
|
|
4
|
+
|
|
5
|
+
Always constructed with a message that includes:
|
|
6
|
+
- What was expected
|
|
7
|
+
- What was actually received
|
|
8
|
+
- A suggested fix
|
|
9
|
+
|
|
10
|
+
Example
|
|
11
|
+
-------
|
|
12
|
+
raise LineSightShapeError(
|
|
13
|
+
"X and y must have the same number of samples.\n"
|
|
14
|
+
f"X has {X.shape[0]} samples, y has {y.shape[0]} samples.\n"
|
|
15
|
+
"Did you forget to align your datasets?"
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
pass
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
|
4
|
+
"""
|
|
5
|
+
Classification accuracy. Used only by LogisticRegression.
|
|
6
|
+
Formula: number of correct predictions / total predictions.
|
|
7
|
+
y_pred should be class labels (0 or 1), not probabilities.
|
|
8
|
+
"""
|
|
9
|
+
return float(np.mean(y_true == y_pred))
|
linesight/metrics/mae.py
ADDED
linesight/metrics/mse.py
ADDED
linesight/metrics/r2.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def r2(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
|
4
|
+
"""
|
|
5
|
+
R-squared (coefficient of determination).
|
|
6
|
+
|
|
7
|
+
Formula: 1 - SS_res / SS_tot
|
|
8
|
+
where SS_res = sum((y_true - y_pred)^2)
|
|
9
|
+
and SS_tot = sum((y_true - mean(y_true))^2)
|
|
10
|
+
|
|
11
|
+
Returns 1.0 for perfect fit.
|
|
12
|
+
Returns 0.0 if model just predicts the mean.
|
|
13
|
+
Can be negative if model is worse than predicting the mean.
|
|
14
|
+
"""
|
|
15
|
+
ss_res = np.sum((y_true - y_pred) ** 2)
|
|
16
|
+
ss_tot = np.sum((y_true - np.mean(y_true)) ** 2)
|
|
17
|
+
if ss_tot == 0:
|
|
18
|
+
# All y values are identical — R^2 is undefined, return 0
|
|
19
|
+
return 0.0
|
|
20
|
+
return float(1 - ss_res / ss_tot)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from linesight.regression.linear.core import LinearRegression
|
|
2
|
+
from linesight.regression.multiple.core import MultipleLinearRegression
|
|
3
|
+
from linesight.regression.ridge.core import RidgeRegression
|
|
4
|
+
from linesight.regression.lasso.core import LassoRegression
|
|
5
|
+
from linesight.regression.elasticnet.core import ElasticNetRegression
|
|
6
|
+
from linesight.regression.logistic.core import LogisticRegression
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from linesight.regression.elasticnet.core import ElasticNetRegression
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from linesight.base import LineSightBase
|
|
3
|
+
from linesight.utils.history import TrainingHistory
|
|
4
|
+
|
|
5
|
+
from linesight.regression.elasticnet.engine.fit import _fit
|
|
6
|
+
from linesight.regression.elasticnet.engine.predict import predict
|
|
7
|
+
from linesight.regression.elasticnet.engine.score import score
|
|
8
|
+
from linesight.regression.elasticnet.engine.get_history import get_training_history
|
|
9
|
+
from linesight.regression.elasticnet.explain.explain_regularization import explain_regularization
|
|
10
|
+
from linesight.regression.elasticnet.explain.explain_coefficients import explain_coefficients
|
|
11
|
+
from linesight.regression.elasticnet.visualization.plot_fit import plot_fit
|
|
12
|
+
from linesight.regression.elasticnet.visualization.plot_loss_curve import plot_loss_curve
|
|
13
|
+
from linesight.regression.elasticnet.visualization.plot_coefficient_shrinkage import plot_coefficient_shrinkage
|
|
14
|
+
from linesight.regression.elasticnet.visualization.compare_regularization_methods import compare_regularization_methods
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
from linesight.regression.elasticnet.visualization.plot_actual_vs_predicted import plot_actual_vs_predicted
|
|
18
|
+
from linesight.regression.elasticnet.visualization.plot_learning_curve import plot_learning_curve
|
|
19
|
+
from linesight.regression.elasticnet.visualization.plot_l1_l2_balance import plot_l1_l2_balance
|
|
20
|
+
|
|
21
|
+
from linesight.regression.elasticnet.visualization.animate_l1_ratio_sweep import animate_l1_ratio_sweep
|
|
22
|
+
from linesight.regression.linear.visualization.plot_residuals import plot_residuals
|
|
23
|
+
|
|
24
|
+
class ElasticNetRegression(LineSightBase):
|
|
25
|
+
"""
|
|
26
|
+
ElasticNet regression: L1 (sparsity) + L2 (shrinkage) combined.
|
|
27
|
+
l1_ratio=1 -> pure Lasso. l1_ratio=0 -> pure Ridge.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
alpha : float, default 1.0
|
|
32
|
+
l1_ratio : float, default 0.5 (must be in [0, 1])
|
|
33
|
+
epochs : int, default 1000
|
|
34
|
+
store_history : bool, default False
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
alpha: float = 1.0,
|
|
40
|
+
l1_ratio: float = 0.5,
|
|
41
|
+
epochs: int = 1000,
|
|
42
|
+
store_history: bool = False,
|
|
43
|
+
normalize: bool = False,
|
|
44
|
+
):
|
|
45
|
+
self.alpha = alpha
|
|
46
|
+
self.l1_ratio = l1_ratio
|
|
47
|
+
self.epochs = epochs
|
|
48
|
+
self.store_history = store_history
|
|
49
|
+
self.normalize = normalize
|
|
50
|
+
self.store_history = store_history
|
|
51
|
+
self.weights = np.zeros(1)
|
|
52
|
+
self.bias = 0.0
|
|
53
|
+
self._history = TrainingHistory()
|
|
54
|
+
self._n_features_in_ = 1
|
|
55
|
+
self._is_fitted = False
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def coef_(self):
|
|
59
|
+
return self.weights
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def intercept_(self):
|
|
63
|
+
return float(self.bias)
|
|
64
|
+
|
|
65
|
+
fit = _fit
|
|
66
|
+
predict = predict
|
|
67
|
+
score = score
|
|
68
|
+
get_training_history = get_training_history
|
|
69
|
+
explain_regularization = explain_regularization
|
|
70
|
+
explain_coefficients = explain_coefficients
|
|
71
|
+
plot_fit = plot_fit
|
|
72
|
+
plot_loss_curve = plot_loss_curve
|
|
73
|
+
plot_coefficient_shrinkage = plot_coefficient_shrinkage
|
|
74
|
+
compare_regularization_methods = compare_regularization_methods
|
|
75
|
+
|
|
76
|
+
plot_actual_vs_predicted = plot_actual_vs_predicted
|
|
77
|
+
plot_learning_curve = plot_learning_curve
|
|
78
|
+
plot_l1_l2_balance = plot_l1_l2_balance
|
|
79
|
+
|
|
80
|
+
animate_l1_ratio_sweep = animate_l1_ratio_sweep
|
|
81
|
+
plot_residuals = plot_residuals
|
|
82
|
+
def summary(self) -> str:
|
|
83
|
+
self._check_fitted("summary")
|
|
84
|
+
converged_str = "Yes" if self._history.converged else "No"
|
|
85
|
+
final_loss = round(self._history.losses[-1], 6) if not self._history.is_empty() else "N/A"
|
|
86
|
+
|
|
87
|
+
lines = [
|
|
88
|
+
"=" * 50,
|
|
89
|
+
f"LineSight — ElasticNetRegression",
|
|
90
|
+
"=" * 50,
|
|
91
|
+
"Training config:",
|
|
92
|
+
f" Alpha: {getattr(self, 'alpha', 'N/A')}",
|
|
93
|
+
f" L1 ratio: {getattr(self, 'l1_ratio', 'N/A')}",
|
|
94
|
+
f" Epochs: {getattr(self, 'epochs', 'N/A')}",
|
|
95
|
+
f" Converged: {converged_str}",
|
|
96
|
+
f" Final loss: {final_loss}",
|
|
97
|
+
"=" * 50,
|
|
98
|
+
]
|
|
99
|
+
output = "\n".join(lines)
|
|
100
|
+
from linesight.utils.environment import _detect_environment
|
|
101
|
+
if _detect_environment() == 'script':
|
|
102
|
+
print(output)
|
|
103
|
+
return output
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def _compute_loss(self, X: np.ndarray, y: np.ndarray) -> float:
|
|
4
|
+
y_pred = np.dot(X, self.weights) + self.bias
|
|
5
|
+
mse_term = float(np.mean((y - y_pred) ** 2))
|
|
6
|
+
l1_term = self.alpha * self.l1_ratio * float(np.sum(np.abs(self.weights)))
|
|
7
|
+
l2_term = self.alpha * (1 - self.l1_ratio) * float(np.sum(self.weights ** 2))
|
|
8
|
+
return mse_term + l1_term + l2_term
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import warnings
|
|
3
|
+
from linesight.utils.validators import _validate_Xy
|
|
4
|
+
from linesight.utils.history import TrainingHistory
|
|
5
|
+
from linesight.exceptions import LineSightConvergenceWarning, LineSightGradientError
|
|
6
|
+
from linesight.regression.elasticnet.engine.soft_threshold import _soft_threshold
|
|
7
|
+
from linesight.regression.elasticnet.engine.compute_loss import _compute_loss
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _fit(self, X, y):
|
|
11
|
+
"""Train ElasticNet with coordinate descent: L1 (sparsity) + L2 (shrinkage)."""
|
|
12
|
+
self._validate_hyperparams()
|
|
13
|
+
|
|
14
|
+
feature_names = list(X.columns) if hasattr(X, "columns") else None
|
|
15
|
+
X, y = _validate_Xy(X, y)
|
|
16
|
+
n_samples, n_features = X.shape
|
|
17
|
+
self._n_features_in_ = n_features
|
|
18
|
+
self.feature_names_in_ = feature_names or [f"x{i}" for i in range(n_features)]
|
|
19
|
+
|
|
20
|
+
# Feature scaling
|
|
21
|
+
if getattr(self, "normalize", False):
|
|
22
|
+
self._X_mean = X.mean(axis=0)
|
|
23
|
+
self._X_std = X.std(axis=0)
|
|
24
|
+
self._X_std[self._X_std == 0] = 1.0
|
|
25
|
+
X = (X - self._X_mean) / self._X_std
|
|
26
|
+
else:
|
|
27
|
+
self._X_mean = None
|
|
28
|
+
self._X_std = None
|
|
29
|
+
|
|
30
|
+
self.weights = np.zeros(n_features)
|
|
31
|
+
self.bias = 0.0
|
|
32
|
+
|
|
33
|
+
losses, weights, biases = [], [], []
|
|
34
|
+
|
|
35
|
+
with np.errstate(over='ignore', invalid='ignore'):
|
|
36
|
+
for epoch in range(self.epochs):
|
|
37
|
+
self.bias = np.mean(y - np.dot(X, self.weights))
|
|
38
|
+
|
|
39
|
+
for j in range(n_features):
|
|
40
|
+
w_j = self.weights[j]
|
|
41
|
+
y_excl_j = np.dot(X, self.weights) - X[:, j] * w_j + self.bias
|
|
42
|
+
rho_j = (1 / n_samples) * np.dot(X[:, j], y - y_excl_j)
|
|
43
|
+
x_j_sq = np.mean(X[:, j] ** 2)
|
|
44
|
+
denom = x_j_sq + self.alpha * (1 - self.l1_ratio)
|
|
45
|
+
self.weights[j] = 0.0 if denom == 0 else _soft_threshold(rho_j, self.alpha * self.l1_ratio) / denom
|
|
46
|
+
|
|
47
|
+
loss = _compute_loss(self, X, y)
|
|
48
|
+
|
|
49
|
+
if not np.isfinite(loss):
|
|
50
|
+
raise LineSightGradientError(
|
|
51
|
+
f"Training failed: loss exploded to infinity at epoch {epoch}. "
|
|
52
|
+
f"Coordinate descent is usually stable, but extremely large feature values can cause overflow. "
|
|
53
|
+
f"Try scaling your features first."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if self.store_history:
|
|
57
|
+
losses.append(loss)
|
|
58
|
+
weights.append(self.weights.copy())
|
|
59
|
+
biases.append(self.bias)
|
|
60
|
+
|
|
61
|
+
converged = True
|
|
62
|
+
if self.store_history and len(losses) >= 2:
|
|
63
|
+
if losses[-1] < losses[-2]:
|
|
64
|
+
converged = False
|
|
65
|
+
warnings.warn("Model hasn't fully converged. Loss still decreasing.", LineSightConvergenceWarning, stacklevel=2)
|
|
66
|
+
|
|
67
|
+
self._history = TrainingHistory(
|
|
68
|
+
losses=losses, weights=weights, biases=biases,
|
|
69
|
+
learning_rate=0.0, epochs_run=self.epochs, converged=converged
|
|
70
|
+
)
|
|
71
|
+
self._is_fitted = True
|
|
72
|
+
return self
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from linesight.exceptions import LineSightDataWarning
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_training_history(self):
|
|
6
|
+
self._check_fitted("get_training_history")
|
|
7
|
+
if self._history.is_empty():
|
|
8
|
+
warnings.warn("Re-fit with store_history=True.", LineSightDataWarning, stacklevel=2)
|
|
9
|
+
return self._history
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from linesight.utils.validators import _validate_X
|
|
3
|
+
|
|
4
|
+
def predict(self, X) -> np.ndarray:
|
|
5
|
+
self._check_fitted("predict")
|
|
6
|
+
X = _validate_X(X, expected_features=self._n_features_in_)
|
|
7
|
+
if getattr(self, "normalize", False) and getattr(self, "_X_mean", None) is not None:
|
|
8
|
+
X = (X - self._X_mean) / self._X_std
|
|
9
|
+
return np.dot(X, self.weights) + self.bias
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from linesight.utils.validators import _validate_Xy
|
|
2
|
+
from linesight.metrics import mse, rmse, mae, r2
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def score(self, X, y) -> dict:
|
|
6
|
+
self._check_fitted("score")
|
|
7
|
+
X, y = _validate_Xy(X, y)
|
|
8
|
+
y_pred = self.predict(X)
|
|
9
|
+
return {
|
|
10
|
+
"mse": round(mse(y, y_pred), 6), "rmse": round(rmse(y, y_pred), 6),
|
|
11
|
+
"mae": round(mae(y, y_pred), 6), "r2": round(r2(y, y_pred), 6),
|
|
12
|
+
"n_samples": int(X.shape[0]), "n_features": int(X.shape[1]),
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def explain_coefficients(self) -> str:
|
|
4
|
+
self._check_fitted("explain_coefficients")
|
|
5
|
+
intercept = round(float(self.bias), 4)
|
|
6
|
+
names = self.feature_names_in_
|
|
7
|
+
|
|
8
|
+
lines = [
|
|
9
|
+
f"Intercept: {intercept}",
|
|
10
|
+
f"",
|
|
11
|
+
f"Feature coefficients (ElasticNet-regularized, alpha={self.alpha}, l1_ratio={self.l1_ratio}):",
|
|
12
|
+
f" Note: Coefficients at exactly 0 were eliminated by the L1 part of the penalty.",
|
|
13
|
+
f"",
|
|
14
|
+
]
|
|
15
|
+
for name, coef in zip(names, self.weights):
|
|
16
|
+
coef_r = round(float(coef), 4)
|
|
17
|
+
if coef_r == 0:
|
|
18
|
+
lines.append(f" {name}: 0 [eliminated]")
|
|
19
|
+
else:
|
|
20
|
+
direction = "increases" if coef_r > 0 else "decreases"
|
|
21
|
+
lines.append(f" {name}: {coef_r}")
|
|
22
|
+
lines.append(f" A 1-unit increase {direction} prediction by {abs(coef_r)}.")
|
|
23
|
+
lines.append("")
|
|
24
|
+
|
|
25
|
+
output = "\n".join(lines)
|
|
26
|
+
from linesight.utils.environment import _detect_environment
|
|
27
|
+
if _detect_environment() == 'script':
|
|
28
|
+
print(output)
|
|
29
|
+
return output
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def explain_regularization(self) -> str:
|
|
4
|
+
"""Explain ElasticNet L1/L2 balance."""
|
|
5
|
+
self._check_fitted("explain_regularization")
|
|
6
|
+
alpha = self.alpha
|
|
7
|
+
l1_ratio = self.l1_ratio
|
|
8
|
+
|
|
9
|
+
l1_penalty = float(np.sum(np.abs(self.weights)))
|
|
10
|
+
l2_penalty = float(np.sum(self.weights ** 2))
|
|
11
|
+
|
|
12
|
+
n_zero = int(np.sum(self.weights == 0))
|
|
13
|
+
n_total = len(self.weights)
|
|
14
|
+
|
|
15
|
+
lines = [
|
|
16
|
+
"ElasticNet Regularization (L1 + L2)",
|
|
17
|
+
"-" * 35,
|
|
18
|
+
f"alpha = {alpha} (Overall regularization strength)",
|
|
19
|
+
f"l1_ratio = {l1_ratio} (Balance between Lasso and Ridge)",
|
|
20
|
+
"",
|
|
21
|
+
"ElasticNet combines both L1 (Lasso) and L2 (Ridge) penalties:",
|
|
22
|
+
" - L1 creates sparsity (sets coefficients to exactly zero)",
|
|
23
|
+
" - L2 shrinks coefficients and handles correlated features better than L1 alone",
|
|
24
|
+
"",
|
|
25
|
+
f"L1 penalty sum(|theta|): {round(l1_penalty, 6)}",
|
|
26
|
+
f"L2 penalty sum(theta^2): {round(l2_penalty, 6)}",
|
|
27
|
+
f"Features set to exactly zero: {n_zero} / {n_total}",
|
|
28
|
+
]
|
|
29
|
+
output = "\n".join(lines)
|
|
30
|
+
from linesight.utils.environment import _detect_environment
|
|
31
|
+
if _detect_environment() == 'script':
|
|
32
|
+
print(output)
|
|
33
|
+
return output
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|