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.
Files changed (188) hide show
  1. linesight/__init__.py +19 -0
  2. linesight/base.py +195 -0
  3. linesight/exceptions/__init__.py +13 -0
  4. linesight/exceptions/convergence_warning.py +9 -0
  5. linesight/exceptions/data_warning.py +6 -0
  6. linesight/exceptions/gradient_error.py +5 -0
  7. linesight/exceptions/not_fitted_error.py +13 -0
  8. linesight/exceptions/shape_error.py +18 -0
  9. linesight/metrics/__init__.py +5 -0
  10. linesight/metrics/accuracy.py +9 -0
  11. linesight/metrics/mae.py +5 -0
  12. linesight/metrics/mse.py +5 -0
  13. linesight/metrics/r2.py +20 -0
  14. linesight/metrics/rmse.py +6 -0
  15. linesight/regression/__init__.py +6 -0
  16. linesight/regression/elasticnet/__init__.py +1 -0
  17. linesight/regression/elasticnet/core.py +103 -0
  18. linesight/regression/elasticnet/engine/__init__.py +0 -0
  19. linesight/regression/elasticnet/engine/compute_loss.py +8 -0
  20. linesight/regression/elasticnet/engine/fit.py +72 -0
  21. linesight/regression/elasticnet/engine/get_history.py +9 -0
  22. linesight/regression/elasticnet/engine/predict.py +9 -0
  23. linesight/regression/elasticnet/engine/score.py +13 -0
  24. linesight/regression/elasticnet/engine/soft_threshold.py +8 -0
  25. linesight/regression/elasticnet/explain/__init__.py +1 -0
  26. linesight/regression/elasticnet/explain/explain_coefficients.py +29 -0
  27. linesight/regression/elasticnet/explain/explain_regularization.py +33 -0
  28. linesight/regression/elasticnet/visualization/__init__.py +1 -0
  29. linesight/regression/elasticnet/visualization/animate_l1_ratio_sweep.py +102 -0
  30. linesight/regression/elasticnet/visualization/compare_regularization_methods.py +100 -0
  31. linesight/regression/elasticnet/visualization/plot_actual_vs_predicted.py +69 -0
  32. linesight/regression/elasticnet/visualization/plot_coefficient_shrinkage.py +43 -0
  33. linesight/regression/elasticnet/visualization/plot_fit.py +36 -0
  34. linesight/regression/elasticnet/visualization/plot_l1_l2_balance.py +113 -0
  35. linesight/regression/elasticnet/visualization/plot_learning_curve.py +127 -0
  36. linesight/regression/elasticnet/visualization/plot_loss_curve.py +27 -0
  37. linesight/regression/lasso/__init__.py +1 -0
  38. linesight/regression/lasso/core.py +105 -0
  39. linesight/regression/lasso/engine/__init__.py +0 -0
  40. linesight/regression/lasso/engine/compute_loss.py +7 -0
  41. linesight/regression/lasso/engine/fit.py +71 -0
  42. linesight/regression/lasso/engine/get_history.py +9 -0
  43. linesight/regression/lasso/engine/predict.py +9 -0
  44. linesight/regression/lasso/engine/score.py +13 -0
  45. linesight/regression/lasso/engine/soft_threshold.py +29 -0
  46. linesight/regression/lasso/explain/__init__.py +0 -0
  47. linesight/regression/lasso/explain/explain_coefficients.py +30 -0
  48. linesight/regression/lasso/explain/explain_regularization.py +30 -0
  49. linesight/regression/lasso/explain/explain_sparsity.py +36 -0
  50. linesight/regression/lasso/visualization/__init__.py +0 -0
  51. linesight/regression/lasso/visualization/animate_coordinate_descent.py +104 -0
  52. linesight/regression/lasso/visualization/compare_with_linear.py +49 -0
  53. linesight/regression/lasso/visualization/plot_actual_vs_predicted.py +69 -0
  54. linesight/regression/lasso/visualization/plot_coefficient_shrinkage.py +44 -0
  55. linesight/regression/lasso/visualization/plot_constraint_region.py +92 -0
  56. linesight/regression/lasso/visualization/plot_feature_elimination.py +88 -0
  57. linesight/regression/lasso/visualization/plot_fit.py +37 -0
  58. linesight/regression/lasso/visualization/plot_learning_curve.py +127 -0
  59. linesight/regression/lasso/visualization/plot_loss_curve.py +28 -0
  60. linesight/regression/lasso/visualization/plot_sparsity_path.py +126 -0
  61. linesight/regression/linear/__init__.py +1 -0
  62. linesight/regression/linear/core.py +134 -0
  63. linesight/regression/linear/engine/__init__.py +0 -0
  64. linesight/regression/linear/engine/compute_loss.py +23 -0
  65. linesight/regression/linear/engine/fit.py +59 -0
  66. linesight/regression/linear/engine/get_history.py +31 -0
  67. linesight/regression/linear/engine/gradient_step.py +42 -0
  68. linesight/regression/linear/engine/predict.py +25 -0
  69. linesight/regression/linear/engine/score.py +38 -0
  70. linesight/regression/linear/explain/__init__.py +0 -0
  71. linesight/regression/linear/explain/explain_coefficients.py +39 -0
  72. linesight/regression/linear/explain/explain_fit.py +66 -0
  73. linesight/regression/linear/explain/show_equation.py +25 -0
  74. linesight/regression/linear/visualization/__init__.py +0 -0
  75. linesight/regression/linear/visualization/animate_loss_surface_path.py +157 -0
  76. linesight/regression/linear/visualization/animate_training.py +109 -0
  77. linesight/regression/linear/visualization/compare_learning_rates.py +97 -0
  78. linesight/regression/linear/visualization/plot_actual_vs_predicted.py +69 -0
  79. linesight/regression/linear/visualization/plot_fit.py +103 -0
  80. linesight/regression/linear/visualization/plot_gradient_vectors.py +145 -0
  81. linesight/regression/linear/visualization/plot_learning_curve.py +126 -0
  82. linesight/regression/linear/visualization/plot_loss_curve.py +78 -0
  83. linesight/regression/linear/visualization/plot_loss_surface.py +135 -0
  84. linesight/regression/linear/visualization/plot_prediction_intervals.py +190 -0
  85. linesight/regression/linear/visualization/plot_residuals.py +74 -0
  86. linesight/regression/linear/visualization/plot_sensitivity_analysis.py +195 -0
  87. linesight/regression/logistic/__init__.py +1 -0
  88. linesight/regression/logistic/core.py +114 -0
  89. linesight/regression/logistic/engine/__init__.py +1 -0
  90. linesight/regression/logistic/engine/compute_loss.py +8 -0
  91. linesight/regression/logistic/engine/fit.py +88 -0
  92. linesight/regression/logistic/engine/get_history.py +8 -0
  93. linesight/regression/logistic/engine/gradient_step.py +21 -0
  94. linesight/regression/logistic/engine/predict.py +9 -0
  95. linesight/regression/logistic/engine/predict_proba.py +12 -0
  96. linesight/regression/logistic/engine/score.py +12 -0
  97. linesight/regression/logistic/engine/sigmoid.py +32 -0
  98. linesight/regression/logistic/explain/__init__.py +1 -0
  99. linesight/regression/logistic/explain/explain_boundary.py +23 -0
  100. linesight/regression/logistic/explain/explain_coefficients.py +27 -0
  101. linesight/regression/logistic/explain/explain_sigmoid.py +21 -0
  102. linesight/regression/logistic/visualization/__init__.py +1 -0
  103. linesight/regression/logistic/visualization/animate_boundary.py +87 -0
  104. linesight/regression/logistic/visualization/plot_actual_vs_predicted.py +69 -0
  105. linesight/regression/logistic/visualization/plot_calibration_curve.py +138 -0
  106. linesight/regression/logistic/visualization/plot_confusion_matrix.py +105 -0
  107. linesight/regression/logistic/visualization/plot_decision_boundary.py +106 -0
  108. linesight/regression/logistic/visualization/plot_learning_curve.py +126 -0
  109. linesight/regression/logistic/visualization/plot_log_odds.py +129 -0
  110. linesight/regression/logistic/visualization/plot_loss_curve.py +27 -0
  111. linesight/regression/logistic/visualization/plot_probability_surface.py +37 -0
  112. linesight/regression/logistic/visualization/plot_residuals.py +30 -0
  113. linesight/regression/logistic/visualization/plot_roc_curve.py +118 -0
  114. linesight/regression/logistic/visualization/plot_sigmoid.py +32 -0
  115. linesight/regression/logistic/visualization/plot_threshold_sensitivity.py +80 -0
  116. linesight/regression/multiple/__init__.py +1 -0
  117. linesight/regression/multiple/core.py +103 -0
  118. linesight/regression/multiple/engine/__init__.py +0 -0
  119. linesight/regression/multiple/engine/compute_loss.py +5 -0
  120. linesight/regression/multiple/engine/fit.py +87 -0
  121. linesight/regression/multiple/engine/get_history.py +13 -0
  122. linesight/regression/multiple/engine/gradient_step.py +23 -0
  123. linesight/regression/multiple/engine/predict.py +11 -0
  124. linesight/regression/multiple/engine/score.py +17 -0
  125. linesight/regression/multiple/explain/__init__.py +0 -0
  126. linesight/regression/multiple/explain/explain_coefficients.py +29 -0
  127. linesight/regression/multiple/explain/show_equation.py +14 -0
  128. linesight/regression/multiple/visualization/__init__.py +0 -0
  129. linesight/regression/multiple/visualization/plot_3d_loss_slice.py +113 -0
  130. linesight/regression/multiple/visualization/plot_actual_vs_predicted.py +69 -0
  131. linesight/regression/multiple/visualization/plot_correlation_matrix.py +35 -0
  132. linesight/regression/multiple/visualization/plot_feature_importance.py +59 -0
  133. linesight/regression/multiple/visualization/plot_fit.py +62 -0
  134. linesight/regression/multiple/visualization/plot_learning_curve.py +126 -0
  135. linesight/regression/multiple/visualization/plot_multicollinearity.py +180 -0
  136. linesight/regression/multiple/visualization/plot_partial_regression.py +67 -0
  137. linesight/regression/multiple/visualization/plot_prediction_plane.py +42 -0
  138. linesight/regression/multiple/visualization/plot_residual_heatmap.py +87 -0
  139. linesight/regression/polynomial/__init__.py +0 -0
  140. linesight/regression/polynomial/core.py +93 -0
  141. linesight/regression/polynomial/engine/__init__.py +0 -0
  142. linesight/regression/polynomial/engine/expand_features.py +30 -0
  143. linesight/regression/polynomial/engine/fit.py +117 -0
  144. linesight/regression/polynomial/engine/predict.py +14 -0
  145. linesight/regression/polynomial/explain/__init__.py +0 -0
  146. linesight/regression/polynomial/explain/show_equation.py +29 -0
  147. linesight/regression/polynomial/visualization/__init__.py +0 -0
  148. linesight/regression/polynomial/visualization/animate_degree_increase.py +88 -0
  149. linesight/regression/polynomial/visualization/compare_degrees.py +110 -0
  150. linesight/regression/polynomial/visualization/plot_actual_vs_predicted.py +69 -0
  151. linesight/regression/polynomial/visualization/plot_basis_functions.py +138 -0
  152. linesight/regression/polynomial/visualization/plot_fit.py +59 -0
  153. linesight/regression/polynomial/visualization/plot_learning_curve.py +126 -0
  154. linesight/regression/ridge/__init__.py +1 -0
  155. linesight/regression/ridge/core.py +109 -0
  156. linesight/regression/ridge/engine/__init__.py +0 -0
  157. linesight/regression/ridge/engine/compute_loss.py +7 -0
  158. linesight/regression/ridge/engine/fit.py +87 -0
  159. linesight/regression/ridge/engine/get_history.py +9 -0
  160. linesight/regression/ridge/engine/gradient_step.py +28 -0
  161. linesight/regression/ridge/engine/predict.py +11 -0
  162. linesight/regression/ridge/engine/score.py +13 -0
  163. linesight/regression/ridge/explain/__init__.py +0 -0
  164. linesight/regression/ridge/explain/explain_coefficients.py +32 -0
  165. linesight/regression/ridge/explain/explain_regularization.py +38 -0
  166. linesight/regression/ridge/visualization/__init__.py +0 -0
  167. linesight/regression/ridge/visualization/animate_regularization.py +100 -0
  168. linesight/regression/ridge/visualization/compare_with_linear.py +58 -0
  169. linesight/regression/ridge/visualization/plot_actual_vs_predicted.py +69 -0
  170. linesight/regression/ridge/visualization/plot_bias_variance_tradeoff.py +110 -0
  171. linesight/regression/ridge/visualization/plot_coefficient_shrinkage.py +75 -0
  172. linesight/regression/ridge/visualization/plot_constraint_region.py +90 -0
  173. linesight/regression/ridge/visualization/plot_effective_degrees_of_freedom.py +117 -0
  174. linesight/regression/ridge/visualization/plot_fit.py +38 -0
  175. linesight/regression/ridge/visualization/plot_learning_curve.py +126 -0
  176. linesight/regression/ridge/visualization/plot_loss_curve.py +30 -0
  177. linesight/utils/__init__.py +13 -0
  178. linesight/utils/array_utils.py +23 -0
  179. linesight/utils/colors.py +43 -0
  180. linesight/utils/environment.py +28 -0
  181. linesight/utils/history.py +16 -0
  182. linesight/utils/validators.py +70 -0
  183. linesight/utils/viz_context.py +51 -0
  184. linesight-0.1.0.dist-info/METADATA +160 -0
  185. linesight-0.1.0.dist-info/RECORD +188 -0
  186. linesight-0.1.0.dist-info/WHEEL +5 -0
  187. linesight-0.1.0.dist-info/licenses/LICENSE +21 -0
  188. 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,9 @@
1
+ import warnings
2
+
3
+ class LineSightConvergenceWarning(UserWarning):
4
+ """
5
+ Issued (not raised) when the model may not have converged.
6
+ Use warnings.warn(..., LineSightConvergenceWarning) to issue it.
7
+ Does not stop execution.
8
+ """
9
+ pass
@@ -0,0 +1,6 @@
1
+ class LineSightDataWarning(UserWarning):
2
+ """
3
+ Issued (not raised) when data has potential issues that will not crash
4
+ the model but may produce poor results.
5
+ """
6
+ pass
@@ -0,0 +1,5 @@
1
+ class LineSightGradientError(Exception):
2
+ """
3
+ Raised when gradient descent diverges and losses go to infinity/NaN.
4
+ """
5
+ pass
@@ -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,5 @@
1
+ from linesight.metrics.mse import mse
2
+ from linesight.metrics.rmse import rmse
3
+ from linesight.metrics.mae import mae
4
+ from linesight.metrics.r2 import r2
5
+ from linesight.metrics.accuracy import accuracy
@@ -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))
@@ -0,0 +1,5 @@
1
+ import numpy as np
2
+
3
+ def mae(y_true: np.ndarray, y_pred: np.ndarray) -> float:
4
+ """Mean Absolute Error. Formula: (1/n) * sum(|y_true - y_pred|)"""
5
+ return float(np.mean(np.abs(y_true - y_pred)))
@@ -0,0 +1,5 @@
1
+ import numpy as np
2
+
3
+ def mse(y_true: np.ndarray, y_pred: np.ndarray) -> float:
4
+ """Mean Squared Error. Formula: (1/n) * sum((y_true - y_pred)^2)"""
5
+ return float(np.mean((y_true - y_pred) ** 2))
@@ -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
+ import numpy as np
2
+ from linesight.metrics.mse import mse
3
+
4
+ def rmse(y_true: np.ndarray, y_pred: np.ndarray) -> float:
5
+ """Root Mean Squared Error. Square root of MSE."""
6
+ return float(np.sqrt(mse(y_true, y_pred)))
@@ -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,8 @@
1
+ def _soft_threshold(rho: float, alpha: float) -> float:
2
+ """Soft-threshold operator for coordinate descent (same as Lasso)."""
3
+ if rho > alpha:
4
+ return rho - alpha
5
+ elif rho < -alpha:
6
+ return rho + alpha
7
+ else:
8
+ return 0.0
@@ -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