Repository: lmfit/lmfit-py

Issue:
Model.fit() gives the wrong 'rsquared' when 'weights' is given

#### First Time Issue Code
Yes, I read the instructions and I am sure this is a GitHub Issue.

#### Description
<!-- Provide a short description of the issue, describe the expected outcome, and give the actual result -->
The ```ModelResult``` instance gained from ```Model.fit()``` has the wrong ```rsquared``` attribute. Floating point ```R^2``` statistic, defined for data ```y``` and best-fit model ```f``` as ```R^2=1-\sum{(y_i-f_i)^2/(y_i-y_mean)^2}```. In model.py, line1482, the code to calculate ```rsquared``` attribute is ```self.rsquared = 1.0 - (self.residual**2).sum()/max(tiny, sstot)```, but in your code, ```residual``` is not ```best_fit-data``` but the return value of the objective function when using the best-fit values of the parameters, which is ```(best_fit-data)*weights```. if ```weights=None``` in ```Model.fit()```, the two are same, otherwise they are different. This is a small bug, but I don't know if there's other part where you use ```residual``` as ```best_fit-data```. Hope you check your code carefully. Thank you for all your contributions.

###### A Minimal, Complete, and Verifiable example
<!-- see, for example, https://stackoverflow.com/help/mcve on how to do this -->
```python
import lmfit as lt
import numpy as np

# Model function
def func(x, k=1, b=0):
    return k*x+b

# function to calculate R^2
def fit_R2(modelresult):
    y = modelresult.data
    f = modelresult.best_fit
    ym = sum(y)/len(y)
    return 1 - sum((y-f)**2)/sum((y-ym)**2)

# data
x = np.array([1, 2, 3, 4])
y = np.array([1.1, 1.9, 3.05, 3.95])
yerr = np.array([0.03, 0.04, 0.01, 0.02])

# fitting
mod = lt.Model(func)
params = mod.make_params()
result = mod.fit(y, params, x=x, weights=1/yerr)
print(result.fit_report())

# comparing
print('\n')
print('R^2 in ModelResult:  ', result.rsquared)
print('My R^2:  ', fit_R2(result))
print('best_fit-data:  ', result.best_fit - result.data )
print('(best_fit-data)*weights:  ', (result.best_fit - result.data ) * result.weights)
print('residual:  ', result.residual)
```

###### Fit report:
<!-- paste the *full* fit report here  -->
```
[[Model]]
    Model(func)
[[Fit Statistics]]
    # fitting method   = leastsq
    # function evals   = 7
    # data points      = 4
    # variables        = 2
    chi-square         = 26.0483871
    reduced chi-square = 13.0241935
    Akaike info crit   = 11.4946460
    Bayesian info crit = 10.2672347
    R-squared          = -4.51288616
[[Variables]]
    k:  0.96370968 +/- 0.04150367 (4.31%) (init = 1)
    b:  0.13774194 +/- 0.12714875 (92.31%) (init = 0)
[[Correlations]] (unreported correlations are < 0.100)
    C(k, b) = -0.9713


R^2 in ModelResult:   -4.51288615804741
My R^2:   0.993748167968698
best_fit-data:   [ 0.00145161  0.16516129 -0.02112903  0.04258065]
(best_fit-data)*weights:   [ 0.0483871   4.12903226 -2.11290323  2.12903226]
residual:   [ 0.0483871   4.12903226 -2.11290323  2.12903226]
```

###### Error message:
<!-- If any, paste the *full* error message inside a code block (starting from line Traceback) -->
There's no error message, but as you see, ```ModelResult``` gives the wrong ```rsquared```.

###### Version information
Python: 3.12.0 (tags/v3.12.0:0fb18b0, Oct  2 2023, 13:03:39) [MSC v.1935 64 bit (AMD64)]

lmfit: 1.2.2, scipy: 1.11.3, numpy: 1.26.0,asteval: 0.9.31, uncertainties: 3.1.7




Discussion:
@ydy1206 Thanks -- yes, it looks like the R^2 value is definitely very wrong when weights are used!
That should be an easy fix, almost certainly https://github.com/lmfit/lmfit-py/blob/c4159900281bdb79d2e3ee22d4b255d82e486f93/lmfit/model.py#L1482
should replace `self.residual` (which, as you say, is weighted) with `dat-self.best_fit` (probably moving the calculation of `self.best_fit` that is just a few lines below.

If you are willing and able to make a Pull Request, that would be great.  If not, let us know and we'll fix this very soon.



Thank you for reply, but I'm sorry I don't know how to make a Pull Request. Actually, I just register my Github account today, and I'm afraid I'll mess it up if I make a Pull Request. 
No worries,  we'll do this.  Thanks!

Resolving patch:
diff --git a/lmfit/model.py b/lmfit/model.py
--- a/lmfit/model.py
+++ b/lmfit/model.py
@@ -1475,15 +1475,16 @@ def fit(self, data=None, params=None, weights=None, method=None,
                 except AttributeError:
                     pass
 
-        if self.data is not None and len(self.data) > 1:
-            dat = coerce_arraylike(self.data)
-            sstot = ((dat - dat.mean())**2).sum()
-            if isinstance(self.residual, np.ndarray) and len(self.residual) > 1:
-                self.rsquared = 1.0 - (self.residual**2).sum()/max(tiny, sstot)
-
         self.init_values = self.model._make_all_args(self.init_params)
         self.best_values = self.model._make_all_args(_ret.params)
         self.best_fit = self.model.eval(params=_ret.params, **self.userkws)
+        if (self.data is not None and len(self.data) > 1
+           and isinstance(self.best_fit, np.ndarray)
+           and len(self.best_fit) > 1):
+            dat = coerce_arraylike(self.data)
+            resid = ((dat - self.best_fit)**2).sum()
+            sstot = ((dat - dat.mean())**2).sum()
+            self.rsquared = 1.0 - resid/max(tiny, sstot)
 
     def eval(self, params=None, **kwargs):
         """Evaluate model function.


Test patch:
diff --git a/tests/test_model.py b/tests/test_model.py
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -1535,3 +1535,20 @@ def lin2(x, k):
     pars.add('m2_k', value=0.5)
     result = ModelSum.fit(y, pars, x=x)
     assert len(result.best_fit) == len(x)
+
+
+def test_rsquared_with_weights():
+    """Github #921"""
+    def func(x, k=1, b=0):
+        return k*x+b
+
+    x = np.array([1, 2, 3, 4])
+    y = np.array([1.1, 1.9, 3.05, 3.95])
+    yerr = np.array([0.03, 0.04, 0.01, 0.02])
+
+    mod = Model(func)
+    params = mod.make_params()
+    result = mod.fit(y, params, x=x, weights=1.0/yerr)
+
+    assert result.rsquared < 1.00
+    assert result.rsquared > 0.95