I think that you are seeing what you are seeing because the model sees the relationship of each set of points in your data frame, which are governed by the equation:
z = 20 + x +3*y + noise
But all the model sees is the resulting
Z, NOT that equation which you know created
So it attempts to build a model which considers how Z was accomplished without knowing there was noise, while KNOWING that both x and y were in this equation because you explicitly told it the are in the model.
Based on this data. (at least this is what I got without a seed, when I ran your data..so it is close with probably different Z due to different noise)
x y z
1 1 32.824550
2 2 21.382597
3 3 80.615424
4 4 30.958157
5 5 42.192197
6 6 75.649622
7 7 29.815352
8 8 40.167267
9 9 59.752065
10 10 53.402601
y are the same for each point and because your formula has
x + 3*y + noise ,
z is also equal to
4*x+ noise or
4*y+ noise or
2*x +2*y+ noise for each row. There are many ways to get the same change to Z with x & y in exact proportion plus some noise.
So the regressors are being assigned equal influence plus an equal share of the noise. It is the most parsimonious evaluation of
It is not what you expect knowing the equation, but it is the answer you should get. If you use a linear model which reduces variables of zero influent, you might even get a 0 or NA value for y.
To test that
lm is working, simply reverse the order of
y changing the relationship between x & y within that equations and you have a completely different result. I think the one you were looking to find.
y = np.arange(100,0,-1)
coef std err t P>|t| [0.025 0.975]
Intercept 0.0439 0.000 119.009 0.000 0.043 0.045
x 1.2184 0.038 32.471 0.000 1.144 1.293
y 3.2130 0.038 85.629 0.000 3.139 3.288
You built your model right, but in this case the data set was not created in a way to test for what you hoped to see...but it was not wrong.