As this is the most popular discussion of the topic I'll add my experience from late 2019-early 2020 here. To add to the existing answers - my focus was to find an accurate AND fast (i.e. vectorized) solution.
Let's start with what is mostly used by answers here - the Haversine approach. It is trivial to vectorize, see example in python below:
def haversine(lat1, lon1, lat2, lon2):
"""
Calculate the great circle distance between two points
on the earth (specified in decimal degrees)
All args must be of equal length.
Distances are in meters.
Ref:
https://stackguides.com/questions/29545704/fast-haversine-approximation-python-pandas
https://ipython.readthedocs.io/en/stable/interactive/magics.html
"""
Radius = 6.371e6
lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])
dlon = lon2 - lon1
dlat = lat2 - lat1
a = np.sin(dlat/2.0)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2.0)**2
c = 2 * np.arcsin(np.sqrt(a))
s12 = Radius * c
# initial azimuth in degrees
y = np.sin(lon2-lon1) * np.cos(lat2)
x = np.cos(lat1)*np.sin(lat2) - np.sin(lat1)*np.cos(lat2)*np.cos(dlon)
azi1 = np.arctan2(y, x)*180./math.pi
return {'s12':s12, 'azi1': azi1}
Accuracy-wise, it is least accurate. Wikipedia states 0.5% of relative deviation on average without any sources. My experiments show less of a deviation. Below is the comparison ran on 100,000 random points vs my library, which should be accurate to millimeter levels:
np.random.seed(42)
lats1 = np.random.uniform(-90,90,100000)
lons1 = np.random.uniform(-180,180,100000)
lats2 = np.random.uniform(-90,90,100000)
lons2 = np.random.uniform(-180,180,100000)
r1 = inverse(lats1, lons1, lats2, lons2)
r2 = haversine(lats1, lons1, lats2, lons2)
print("Max absolute error: {:4.2f}m".format(np.max(r1['s12']-r2['s12'])))
print("Mean absolute error: {:4.2f}m".format(np.mean(r1['s12']-r2['s12'])))
print("Max relative error: {:4.2f}%".format(np.max((r2['s12']/r1['s12']-1)*100)))
print("Mean relative error: {:4.2f}%".format(np.mean((r2['s12']/r1['s12']-1)*100)))
Output:
Max absolute error: 26671.47m
Mean absolute error: -2499.84m
Max relative error: 0.55%
Mean relative error: -0.02%
So on average 2.5km deviation on 100,000 random pairs of coordinates, which may be good for majority of cases.
Next option is Vincenty's formulae which is accurate up to millimeters, depending on convergence criteria and can be vectorized as well. It does have the issue with convergence near antipodal points. You can make it converge at those points by relaxing convergence criteria, but accuracy drops to 0.25% and more. Outside of antipodal points Vincenty will provide results close to Geographiclib within relative error of less than 1.e-6 on average.
Geographiclib, mentioned here, is really the current golden standard. It has several implementations and fairly fast, especially if you are using C++ version.
Now, if you are planning to use Python for anything above 10k points I'd suggest to consider my vectorized implementation. I created a geovectorslib library with vectorized Vincenty routine for my own needs, which uses Geographiclib as fallback for near antipodal points. Below is the comparison vs Geographiclib for 100k points. As you can see it provides up to 20x improvement for inverse and 100x for direct methods for 100k points and the gap will grow with number of points. Accuracy-wise it will be within 1.e-5 rtol of Georgraphiclib.
Direct method for 100,000 points
94.9 ms ± 25 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
9.79 s ± 1.4 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
Inverse method for 100,000 points
1.5 s ± 504 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
24.2 s ± 3.91 s per loop (mean ± std. dev. of 7 runs, 1 loop each)