Plotting in python for Physics students

Edward Sternin
2025-09

What we want is...

This is a basic introduction, and it should be sufficient to get you going in your first- and second-year labs. Mastering python is a long process, and one should not expect to become an expert overnight. However, basics can be learned quickly, and using only a handful of commands.

Here's a dataset, as measured in the lab, of a voltage across and a current through a load resistor connected to an unknown source. As we vary the resistance of the load, the following set of measurements is obtained:

#  V   I,mA
  0   0.468
  1   0.405
  2   0.342
  3   0.279
  4   0.216
  5   0.153
  6   0.090
  6.4 0.064

What does it look like? How can we "plot $I$ along the $y$-axis as a function of $V$ along the $x$-axis"? Perhaps:

  plot(V,I)

Turns out, this is almost it. Two things need to happen first:

We just grabbed the first tool that came to mind out of the toolbox, and for a quick-and-dirty look at the data this may be good enough. But like all toolboxes, ours is stuffed with goodies, and we can quickly get better than our first primitive plot. Also, Ohm's Law is boringly linear, a straight line. Let's look instead at the power dissipated $P=VI=I^2R=V^2/R$ as a function of $V$, it should give us a parabola.

But first, let's get a bigger toolbox:

We just imported everything (*) from two very large libraries, and now our python session knows a lot of cool commands and functions. If you were to do

dir()

at this point, a very long list of functions would appear.

Some functions are about plotting (matplotlib), some are about numerics (numpy) like sin() and cos() or multiply():

We had to call a multiply() function from the numpy toolbox/library because multiplying two vectors is tricky: you need to specify whether it's a dot-product, a cross-product, or as here: an element-by-element product.

Also, we can start making our plot a bit better-looking, as well as telling a more complex story from the same experimental data. After all, we performed the measurement in the laboratory by changing the value of the load resistance, which of course is $R=V/I$, so how does power depends on the load resistance?

There is an obvious maximum in the graph. This is the point where the impedance of the load $R$ matches the internal impedance $R_0$ of the source $V_0$, and the measured $V$ across $R$ at this point is exactly $\frac{1}{2}V_0$. At the extreme of current going to zero, there is a zero voltage drop across internal $R_0$ and so $V \approx V_0$. Looking at the data table yields a guesstimate of $V_0 \approx 6.5$ and $R_0 = 3.25\mbox{ V} / 0.25 \mbox{ mA} = 13\Omega$.

We expect the power dissipated on $R$ to be $$ P = I^2R = \left( \frac{V_0}{R+R_0} \right)^2 R $$

Let's try to add this theoretical prediction to the plot.

We need to make a small change because of a python quirk. Since we are going to perform computations on an array or, mathematically speaking, a vector of $R$ values, we need to use a python array(), not a python list [a, b, ...]. Lists are more general objects in python and may include mixtures of things, like numbers and strings intermixed, but arrays are of numbers only. The side benefit of converting to arrays is that we no longer need to use multiply() and divide(), the usual * and / will suffice.

Since we want our theoretical curve to look smooth, we will also generate another array of all possible $R$ values, from the minimum to the maximum of the values we used in the experiment, but at a much finer step size, using function arange(min,max,step). The resulting curve should look smooth in-between the data points, so we use a "blue line" plot 'b-' instead of a "red point" 'r.' scatter plot we used for the experimental data.

This is pretty good already, but let us refine our initial guesstimates of $V_0$ and $R_0$ values by treating them as parameters of some function and then performing a least-squares fit of the function to the data.

numpy already has some linear algebra tools (from the numpy toolbox) that we could use, but we can also import another toolbox, scipy, that has some more powerful and flexible tools, including non-linear least-squares curve fitting function. When you follow the link to read up on how to call this tool, you will see a lot of powerful options available to you that control the exact details of how the fit is performed. In our case, we will use only the simplest form of the call to our Swiss-army-knife of a function, curve_fit(). Putting it all together:

Details in the above script

Other useful things

Reading data from files

Using jupyter magic, create a data file on the fly, then read it into python. This is equivalent to the first two lines of the above script, and allows one to separate the actions of the script from the data these actions apply to. If the script has a simple loop over a list of file names, it can perform the same actions on a large number of data files in one go.

Error bars

In the same manner, if the third column in the file were to contain error bars for every data point, is would be easy to add dI = data[:,2], and use errorbar() instead of plot() with yerr = dI included in the calling sequence: dI = data[:,2] errorbar(V,I, fmt='ro', yerr = dI);

In other cases, only the instrumental error is known, the same for all measurements. errorbar() is flexible enough that is can accept both a vector and a scalar for errorbars, it automatically assigns the same scalar number (say, 0.02 i.e. 2 in the second digit after the decimal of $I$) to all errorbars: errorbar(V,I, fmt='ro', yerr = 0.02);

Alternatively, one could create a vector of the same size as the data vector, but with all values set to that instrumental error. One can use standard python declarattions, like this: dI = array([0.02]*len(I))

or one might prefer to do this vector multiplication by a scalar to achieve the same goal: dI = 0.0*I + 0.02

You can also have asymmetric errorbars, different below and above the data values. And, of course, you can also add xerr = ... in the same way.

Multiple axes, multiple plots on a figure

When two graphs display very different quantities that cannot be shown on the same scale, there are two options: overlaying the two plots on the same graph, but providing a separate vertical axis for each of the two quantities, or stacking two separate graphs on the same figure.

Note how providing an array to c= (colour) or to s= (size) settings in the scatter() command changes colour or size to marker symbols at every data point.

Many more things

Many other aspects of the graph appearance can be controlled. For some of them, control is simply a matter of inserting a pair keyword=value into the calling sequence of one of the plotting commands, for some it requires invoking a different command altogether (e.g. contour()). This simple introduction just covered the basics; for more - RTFM ("read the fine manual").

Happy trails!