はじめに
実験結果やアンケート、センサーデータなどを要約するとき、最初に計算することが多いのが平均値です。
平均値はとても便利です。たくさんの数値を、たった1つの数値に要約できます。
しかし、1つに要約するということは、それ以外の情報を捨てることでもあります。たとえば、次のような情報は平均値だけでは分かりません。
- データが平均値の近くに集まっているのか
- データが広い範囲に散らばっているのか
- 山が1つなのか、2つの集団に分かれているのか
- 一部に非常に大きな値があるのか
- ある基準値を超えるデータがどれくらいあるのか
この記事では、平均値がすべて50になる4種類のダミーデータをPythonで作ります。そして、平均値、中央値、標準偏差、ヒストグラム、箱ひげ図を順に見ながら、「平均値が同じでもデータの中身は大きく異なる」ことを確認します。
この記事のコードはGoogle Colabでそのまま実行できます。
使用するデータは、説明のために作成した架空のダミーデータです。単位にも実験上の意味はありません。
この記事のゴール
この記事のゴールは、次の4点です。
- 平均値、中央値、標準偏差、分布という用語を理解する
- 同じ平均値を持つ、形の異なるダミーデータを作る
- 平均値だけのグラフと、分布を示すグラフを比較する
- 実データを見るときの確認項目を身につける
まず結論
平均値は重要ですが、平均値だけではデータの全体像は分かりません。
少なくとも、次の情報を一緒に確認することが大切です。
- データ数 $n$
- 中心を表す平均値や中央値
- ばらつきを表す標準偏差や四分位範囲
- ヒストグラムや個々のデータ点から分かる分布の形
特に、分布が歪んでいる場合、複数の山がある場合、極端な値がある場合には、平均値だけを見て判断すると重要な特徴を見落とすことがあります。
用語を確認する
データ点と標本サイズ
1回の測定で得られた1つの値を、ここではデータ点と呼びます。
データ点の個数を標本サイズまたはサンプルサイズと呼び、通常は $n$ で表します。
たとえば、200個の値があれば $n=200$ です。
分布
分布とは、どのあたりの値が多いのか、どの程度広がっているのか、山がいくつあるのか、といったデータ全体の形です。
同じ平均値を持つデータでも、分布は同じとは限りません。
平均値
$n$ 個のデータを $x_1, x_2, \ldots, x_n$ とすると、平均値 $\bar{x}$ は次のように定義されます。
$$
\bar{x}=
\frac{1}{n}
\sum_{i=1}^{n} x_i
$$
すべての値を足し、データ数で割った値です。
中央値
中央値は、データを小さい順に並べたときの中央の値です。
平均値は非常に大きい値や非常に小さい値の影響を受けやすい一方、中央値はその影響を比較的受けにくいという特徴があります。
ただし、中央値も分布の形を1つの数値に要約したものです。中央値だけで山の数や広がりが分かるわけではありません。
標準偏差
標準偏差は、データが平均値の周りにどれくらい広がっているかを表す代表的な指標です。
この記事で使う標本標準偏差 $s$ は、次のように表されます。
$$
s=
\sqrt{
\frac{1}{n-1}
\sum_{i=1}^{n}
(x_i-\bar{x})^2
}
$$
標準偏差が小さいほどデータは平均値の近くに集まり、標準偏差が大きいほど広い範囲に散らばります。
四分位数と四分位範囲
データを小さい順に並べ、下から25%の位置を第1四分位数 $Q_1$、50%の位置を中央値 $Q_2$、75%の位置を第3四分位数 $Q_3$ と呼びます。
中央の50%のデータが含まれる幅を四分位範囲と呼びます。
$$
IQR = Q_3 - Q_1
$$
四分位範囲は、箱ひげ図でデータの広がりを見るときに使われます。
今回作る4種類のダミーデータ
今回は、各群に200個の値を作ります。どの群も平均値が50になるように調整しますが、分布の形は変えます。
| 群 | 特徴 | 見たいポイント |
|---|---|---|
| A: narrow | 50付近に狭く集まる | ばらつきが小さい |
| B: wide | 50を中心に広く散らばる | ばらつきが大きい |
| C: bimodal | 35付近と65付近に2つの山を持つ | 平均値付近のデータが少ない |
| D: right-skewed | 右側に長い裾を持つ | 大きな値が平均値を押し上げる |
1. ダミーデータを作る
Google Colabを開き、最初のセルに次のコードを貼り付けて実行します。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
from IPython.display import display
# -----------------------------
# 1. 再現性のための設定
# -----------------------------
SEED = 42
rng = np.random.default_rng(SEED)
n = 200
target_mean = 50.0
# 図の保存先
figure_dir = Path("figures")
figure_dir.mkdir(exist_ok=True)
# -----------------------------
# 2. 平均値を指定した値に平行移動する関数
# -----------------------------
def shift_to_mean(x, target=50.0):
x = np.asarray(x, dtype=float)
return x - x.mean() + target
# -----------------------------
# 3. 4種類のダミーデータを作る
# -----------------------------
# A: 50付近に狭く集まる
group_a = shift_to_mean(
rng.normal(loc=50, scale=3, size=n),
target_mean
)
# B: 50を中心に広く散らばる
group_b = shift_to_mean(
rng.normal(loc=50, scale=12, size=n),
target_mean
)
# C: 35付近と65付近に2つの山を持つ
group_c = np.concatenate([
rng.normal(loc=35, scale=3, size=n // 2),
rng.normal(loc=65, scale=3, size=n // 2),
])
rng.shuffle(group_c)
group_c = shift_to_mean(group_c, target_mean)
# D: 右側に長い裾を持つ
group_d = shift_to_mean(
rng.exponential(scale=8, size=n),
target_mean
)
groups = {
"A: narrow": group_a,
"B: wide": group_b,
"C: bimodal": group_c,
"D: right-skewed": group_d,
}
# 縦長形式のDataFrameにまとめる
df = pd.DataFrame({
"group": np.repeat(list(groups.keys()), n),
"value": np.concatenate(list(groups.values()))
})
# -----------------------------
# 4. 要約統計量を計算する
# -----------------------------
summary = (
df.groupby("group", sort=False)["value"]
.agg(
n="count",
mean="mean",
median="median",
std="std",
min="min",
q1=lambda s: s.quantile(0.25),
q3=lambda s: s.quantile(0.75),
max="max",
)
.round(2)
)
display(summary)
このコードでは、いったん作ったデータから元の平均値を引き、50を加えています。
各データ点を $x_i$、元の平均値を $\bar{x}$ とすると、変換後の値は次のようになります。
$$
x_i' = x_i - \bar{x} + 50
$$
変換後の平均値は、
$$
\overline{x'}=
\bar{x}-\bar{x}+50=
50
$$
です。
この操作は、分布の形やデータ同士の間隔を変えず、分布全体を左右に平行移動します。そのため、「平均値だけを同じにして、分布の違いを比較する」という今回の目的に使えます。
上のコードを実行すると、次の要約統計量が得られます。
| group | n | mean | median | std | min | q1 | q3 | max |
|---|---|---|---|---|---|---|---|---|
| A: narrow | 200 | 50.00 | 49.94 | 2.65 | 43.70 | 48.12 | 51.70 | 58.83 |
| B: wide | 200 | 50.00 | 50.48 | 12.23 | 18.96 | 41.40 | 57.43 | 84.62 |
| C: bimodal | 200 | 50.00 | 49.65 | 15.28 | 28.20 | 35.02 | 64.94 | 72.47 |
| D: right-skewed | 200 | 50.00 | 47.19 | 8.78 | 41.83 | 43.86 | 53.36 | 101.55 |
4群の平均値は、すべて50です。
しかし、標準偏差は約2.65から約15.28まで異なっています。また、D群では平均値が50であるのに対し、中央値は約47.19です。右側の大きな値が平均値を引き上げていることが、この時点ですでに分かります。
2. 平均値だけを棒グラフで見る
次のセルを実行します。
means = df.groupby("group", sort=False)["value"].mean()
plt.figure(figsize=(8, 4.5))
bars = plt.bar(means.index, means.values)
plt.axhline(
target_mean,
linestyle="--",
linewidth=1,
label="target mean = 50"
)
plt.ylim(0, 60)
plt.ylabel("Mean")
plt.title("The four groups have the same mean")
plt.xticks(rotation=15, ha="right")
for bar, value in zip(bars, means.values):
plt.text(
bar.get_x() + bar.get_width() / 2,
value + 0.8,
f"{value:.1f}",
ha="center",
va="bottom"
)
plt.legend()
plt.tight_layout()
plt.savefig(
figure_dir / "figure_01_mean_only.png",
dpi=200,
bbox_inches="tight"
)
plt.show()
図1. 4群の平均値。すべて50であり、平均値だけを見ると群間の違いは分からない。
この棒グラフだけを見れば、4群はほとんど同じデータに見えます。
しかし、ここで分かるのは「4群の平均値が同じ」ということだけです。各群のデータがどれくらい散らばっているか、どのような形をしているかは分かりません。
平均値は間違っていません。ただし、平均値だけでは答えられない問いが多いということです。
3. ヒストグラムで分布を見る
ヒストグラムは、値の範囲をいくつかの区間に分け、それぞれの区間に何個のデータが入っているかを棒の高さで表す図です。
次のセルを実行します。
# 全群を同じ横軸と区間で比較する
x_min = np.floor(df["value"].min() / 5) * 5
x_max = np.ceil(df["value"].max() / 5) * 5
bins = np.linspace(x_min, x_max, 31)
fig, axes = plt.subplots(
2, 2,
figsize=(10, 7),
sharex=True,
sharey=True
)
for ax, (name, values) in zip(axes.ravel(), groups.items()):
ax.hist(
values,
bins=bins,
edgecolor="black",
alpha=0.75
)
ax.axvline(
values.mean(),
linestyle="-",
linewidth=2,
label=f"mean = {values.mean():.1f}"
)
ax.axvline(
np.median(values),
linestyle="--",
linewidth=2,
label=f"median = {np.median(values):.1f}"
)
ax.set_title(name)
ax.set_xlabel("Value")
ax.set_ylabel("Count")
ax.legend(fontsize=8)
fig.suptitle(
"Same mean, different distributions",
fontsize=14
)
fig.tight_layout()
plt.savefig(
figure_dir / "figure_02_histograms.png",
dpi=200,
bbox_inches="tight"
)
plt.show()
図2. 平均値が同じ4群のヒストグラム。実線は平均値、破線は中央値を表す。
平均値はすべて50ですが、分布の形は大きく異なります。
A: narrow
データの多くが50付近に集まっています。標準偏差は約2.65で、4群の中で最も小さい値です。
この場合、平均値50はデータの代表値として比較的自然に見えます。
B: wide
平均値はA群と同じ50ですが、20付近から80付近まで広く散らばっています。標準偏差は約12.23です。
A群とB群を平均値だけで比較すると、この大きなばらつきの違いを見落とします。
C: bimodal
35付近と65付近に2つの山があります。このように山が2つある分布を二峰性分布と呼びます。
平均値は50ですが、50付近のデータはむしろ少なくなっています。
この例では、平均値50は数学的には正しい一方で、「典型的なデータ点」をうまく表しているとは言いにくい状態です。2つの異なる集団や状態が混ざっていないかを考える必要があります。
D: right-skewed
40台の値が多い一方、右側に100付近まで続く長い裾があります。このような形を右に歪んだ分布または右裾の長い分布と呼びます。
D群では平均値が50、中央値が約47.19です。右側の大きな値が平均値を押し上げるため、平均値が中央値より大きくなっています。
ただし、「平均値と中央値が違えば必ず問題」という意味ではありません。違いが生まれた理由を、分布と合わせて確認することが大切です。
ヒストグラムの見え方は、区間の数や幅によって変わります。
区間の設定を変えても主要な特徴が保たれるかを確認すると、より安全に解釈できます。
4. 箱ひげ図と個々のデータ点を見る
箱ひげ図は、中央値と四分位数を使って分布をコンパクトに表す図です。
- 箱の下端は $Q_1$
- 箱の中の線は中央値 $Q_2$
- 箱の上端は $Q_3$
- 箱の高さは四分位範囲 $IQR$
- Matplotlibの標準設定では、ひげは原則として $Q_1-1.5IQR$ から $Q_3+1.5IQR$ の範囲内にある最遠のデータ点まで伸びる
今回は箱ひげ図だけでなく、すべてのデータ点も重ねます。横方向に少しだけずらしているのは、点同士の重なりを減らすためです。このずらしにデータ上の意味はありません。
fig, ax = plt.subplots(figsize=(9, 5.5))
data_list = list(groups.values())
positions = np.arange(1, len(data_list) + 1)
ax.boxplot(
data_list,
positions=positions,
widths=0.55,
showmeans=True,
meanline=True,
showfliers=False,
meanprops={
"linestyle": "-",
"linewidth": 2
},
medianprops={
"linestyle": "--",
"linewidth": 2
},
)
# 点の重なりを減らすための横方向の小さな揺らぎ
rng_jitter = np.random.default_rng(123)
for pos, values in zip(positions, data_list):
jitter = rng_jitter.normal(
loc=0,
scale=0.055,
size=len(values)
)
ax.scatter(
np.full(len(values), pos) + jitter,
values,
s=12,
alpha=0.35
)
ax.axhline(
target_mean,
linestyle=":",
linewidth=1
)
ax.set_xticks(positions)
ax.set_xticklabels(
groups.keys(),
rotation=15,
ha="right"
)
ax.set_ylabel("Value")
ax.set_title("Boxplots and individual data points")
ax.text(
0.99,
0.02,
"solid line: mean / dashed line: median",
transform=ax.transAxes,
ha="right",
va="bottom",
fontsize=9
)
fig.tight_layout()
plt.savefig(
figure_dir / "figure_03_boxplot_points.png",
dpi=200,
bbox_inches="tight"
)
plt.show()
図3. 箱ひげ図と個々のデータ点。実線は平均値、破線は中央値を表す。
この図では、次の違いを一度に確認できます。
- A群は狭い範囲に集まっている
- B群は広い範囲に連続的に散らばっている
- C群は上下の2か所に集団がある
- D群は右側に少数の大きな値を持つ
重要なのは、箱ひげ図だけでも分布のすべてが分かるわけではないことです。
たとえばC群の箱は大きく見えますが、箱だけでは「2つの山に分かれている」という特徴が十分に伝わりません。個々のデータ点やヒストグラムを合わせて見ることで、二峰性が分かりやすくなります。
また、箱ひげ図のひげより外側にある点は、機械的には「外れ値候補」として扱われることがあります。しかし、それだけで測定ミスや削除すべき値と決めることはできません。D群では、右側の大きな値も今回設定した分布から正しく生成されたデータです。
5. 同じ平均値でも、基準値を超える割合は違う
分布の違いは、実際の判断にも影響します。
例として、値が60以上のデータを「高い値」とする仮の基準を置きます。この60という値には、医学的・科学的な意味はありません。説明のための仮の基準です。
threshold = 60
high_rates = (
df.assign(high=df["value"] >= threshold)
.groupby("group", sort=False)["high"]
.mean()
.mul(100)
)
plt.figure(figsize=(8, 4.5))
bars = plt.bar(
high_rates.index,
high_rates.values
)
plt.ylabel(
f"Percentage of values >= {threshold} (%)"
)
plt.title(
f"Same mean, different percentages above {threshold}"
)
plt.xticks(rotation=15, ha="right")
plt.ylim(0, high_rates.max() * 1.25)
for bar, value in zip(bars, high_rates.values):
plt.text(
bar.get_x() + bar.get_width() / 2,
value + 1,
f"{value:.1f}%",
ha="center",
va="bottom"
)
plt.tight_layout()
plt.savefig(
figure_dir / "figure_04_threshold_rates.png",
dpi=200,
bbox_inches="tight"
)
plt.show()
display(high_rates.round(1).rename("percentage"))
図4. 値が60以上であるデータの割合。平均値は同じでも、割合は大きく異なる。
結果は次のようになります。
| group | 60以上の割合 |
|---|---|
| A: narrow | 0.0% |
| B: wide | 19.0% |
| C: bimodal | 48.0% |
| D: right-skewed | 11.0% |
4群の平均値はすべて50です。
それにもかかわらず、60以上の割合は0%から48%まで異なります。
この例から、平均値だけでは次のような問いに答えられないことが分かります。
- 基準値を超えるデータは何個あるか
- 極端に大きな値は存在するか
- データは1つの集団なのか
- 多くのデータ点は平均値の近くにあるのか
平均値が同じだからといって、実務上の意味まで同じとは限りません。
平均値は使わないほうがよいのか?
そうではありません。
平均値は、データの中心を表す重要な指標です。特に、分布が単峰性で大きく歪んでおらず、極端な値が少ない場合には、分かりやすい代表値になります。
問題は、平均値を使うことではなく、平均値だけでデータ全体を説明したつもりになることです。
平均値を見るときは、少なくとも次の観点を組み合わせると安全です。
| 確認したいこと | 指標・図の例 |
|---|---|
| データ数 | $n$ |
| 中心はどこか | 平均値、中央値 |
| どれくらい散らばるか | 標準偏差、四分位範囲、範囲 |
| どのような形か | ヒストグラム、密度図、個々のデータ点 |
| 複数の集団が混ざっていないか | ヒストグラム、散布図、群別表示 |
| 極端な値があるか | 個々のデータ点、箱ひげ図 |
| 基準値を超える割合はどうか | 件数、割合、累積分布 |
実データを可視化するときのチェックリスト
実際の研究データや業務データでは、次の順番で確認すると整理しやすくなります。
- データ数 $n$ を確認する
- 欠損値があるか確認する
- 平均値と中央値を比較する
- 標準偏差や四分位範囲でばらつきを確認する
- ヒストグラムや個々の点を描く
- 複数の山、歪み、長い裾がないか確認する
- 極端な値の原因を確認する
- 必要なら群や条件ごとに分けて描く
極端な値を見つけても、理由を確認せず機械的に削除しないことが重要です。測定ミスかもしれませんが、まれに起こる重要な現象かもしれません。
また、データ数が少ない場合には、棒グラフだけでなく個々のデータ点を示すと、データの実態を把握しやすくなります。
試してみる課題
コードの一部を変更すると、理解をさらに深められます。
課題1:データ数を変える
n = 20
としてデータを作り直し、$n=200$ の場合と図の安定性を比べてみてください。
課題2:B群のばらつきを変える
rng.normal(loc=50, scale=20, size=n)
として、標準偏差とヒストグラムがどう変わるか確認します。
課題3:D群に非常に大きな値を1つ追加する
group_d[0] = 200
として、平均値、中央値、標準偏差への影響を比較します。
課題4:基準値を変える
threshold = 55
または、
threshold = 70
として、基準値以上の割合がどう変わるか確認します。
課題5:平均値を50にそろえない
shift_to_mean() を外し、自然に生成された標本平均がどの程度ばらつくか確認します。
まとめ
この記事では、平均値がすべて50になる4種類のダミーデータを作りました。
- 平均値が同じでも、ばらつきは異なる
- 平均値が同じでも、分布の山の数は異なる
- 歪んだ分布では、平均値と中央値がずれることがある
- 平均値付近に実際のデータが少ない場合もある
- 平均値が同じでも、基準値を超える割合は大きく異なる
平均値は、データを理解するための出発点です。しかし、それだけで結論を出すのではなく、中央値、ばらつき、分布の形、個々のデータ点を合わせて確認することが大切です。
平均値を見る。次に、分布を見る。
この一手間だけでも、データの読み違いをかなり減らせます。



