This is due to how the statistics package tries to give you consistent outputs depending on the numeric type you pass in, so it handles int, float, decimal.Decimal, fractions.Fraction as you would hope. Unfortunately, numpy types don't play well with the python numeric type hierarchy. So we can look at the source code (this is the Python version, your runtime is probably using a fast, C version but they should work equivalently...):
def mean(data):
"""Return the sample arithmetic mean of data.
>>> mean([1, 2, 3, 4, 4])
2.8
>>> from fractions import Fraction as F
>>> mean([F(3, 7), F(1, 21), F(5, 3), F(1, 3)])
Fraction(13, 21)
>>> from decimal import Decimal as D
>>> mean([D("0.5"), D("0.75"), D("0.625"), D("0.375")])
Decimal('0.5625')
If ``data`` is empty, StatisticsError will be raised.
"""
if iter(data) is data:
data = list(data)
n = len(data)
if n < 1:
raise StatisticsError('mean requires at least one data point')
T, total, count = _sum(data)
assert count == n
return _convert(total/n, T)
So, essentially it uses a type-aware sum, which returns the type, total, and count. Essentially, the total/count is coerced to T. Notice:
In [28]: T, total, count = statistics._sum(np.array([1,2,3]))
In [29]: T, total, count
Out[29]: (numpy.int64, Fraction(6, 1), 3)
In [30]: total / count
Out[30]: Fraction(2, 1)
In [31]: T(total / count)
Out[31]: 2
Notice, all the objects integers you see here are actually numpy.int64, not vanilla int objects. But why doesn't this happen when we do statistics.mean([1,2,3,4])? Well, because the library was built assuming normal python numeric types, peeking at the _convert function:
def _convert(value, T):
"""Convert value to given numeric type T."""
if type(value) is T:
# This covers the cases where T is Fraction, or where value is
# a NAN or INF (Decimal or float).
return value
if issubclass(T, int) and value.denominator != 1:
T = float
try:
# FIXME: what do we do if this overflows?
return T(value)
except TypeError:
if issubclass(T, Decimal):
return T(value.numerator)/T(value.denominator)
else:
raise
You'll notice, it special cases: if issubclass(T, int) and value.denominator != 1, i.e., you have an int and the denominator isn't one, so you want a float:
T = float
HOWEVER:
In [36]: issubclass(np.int64, int)
Out[36]: False
So, T is just np.int64, and:
In [37]: total / count
Out[37]: Fraction(2, 1)
In [38]: np.int64(total / count)
Out[38]: 2