ビジネス多変量解析:ドラッグストア事例(前編)
売上を左右する要因は何か

ビジネス多変量解析:ドラッグストア事例(前編)売上を左右する要因は何か

あなたは全国にドラッグストアを展開する企業の経営企画部に配属されました。

あなたは上司から……

来年の新規出店候補が10ヵ所ある。
 どこに出店すれば売上が見込めるか、
 データに基づいて優先順位をつけてほしい。

ついでに、
 既存店舗の売上をもっと伸ばすために
 何に投資すべきかも分析してくれ。

……と言われています。

今回は、この課題に回帰分析で挑む第一歩として、データの探索(EDA)から重回帰分析の構築、そして多重共線性の診断までを実施していきます。

この課題の2つの側面

この事例の上司から与えられた課題には2つの側面があります。

ひとつは予測です。

新しい立地で売上がどの程度になるかを推定すること。

もうひとつは要因分析です。

売上を左右している要因を特定し、その影響度を定量化することです。

回帰分析は、まさにこの2つの目的を同時に達成できる手法です。

ただし実際のビジネスデータでは、候補となる変数が数十個にのぼることも珍しくありません。

立地の特性(人口密度、最寄駅の乗降客数、競合店の数)、店舗の属性(売場面積、駐車場台数、スタッフ数)、商圏の情報(世帯年収、高齢者比率、周辺施設)など、データベースから引っ張ってこられる変数は無数にあります。

これらすべてをモデルに投入すると、多重共線性の問題や過学習の問題が生じます。

今回の前編では、データの準備と探索的分析(EDA)を行い、全変数を投入した重回帰分析を構築し、多重共線性の診断までを扱います。

次回の後編では、Lasso回帰による変数選択やモデル評価、そしてビジネスレポートの作成に進みます。

 

準備

 ライブラリのインポート

今回使用するライブラリをまとめてインポートします。

回帰分析のためにstatsmodels(統計的な詳細を確認するため)とscikit-learn(モデル構築と評価のため)の2つのライブラリを使いま。

variance_inflation_factorは多重共線性を診断するためのVIF計算用です。

以下、コードです。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import japanize_matplotlib
import statsmodels.api as sm

from sklearn.preprocessing import StandardScaler
from statsmodels.stats.outliers_influence import variance_inflation_factor

 

 サンプルデータの読み込み

サンプルデータとして、150店舗 × 11変数(目的変数1+説明変数10)ののドラッグストアデータを利用します。

サンプルデータは、以下からダウンロードできます。
drugstore_data.csv

 

以下、コードです。

# 読み込むCSVファイル名
csv_path = 'drugstore_data.csv'
# データの読み込み(CSVからDataFrameを作成)
df = pd.read_csv(csv_path)

print(f"店舗データ: {len(df)} 店舗 × {len(df.columns)} 変数") 
print("【先頭5店舗のデータ】")
print(df.head())

 

以下、実行結果です。

店舗データ: 150 店舗 × 12 変数
【先頭5店舗のデータ】
   store_id  monthly_sales  sales_area   pop_density  station_passengers  \
0         0    9912.185172  368.281211  13016.146613        35277.142025   
1         1    9614.122280  350.012969  14329.963779        58835.854506   
2         2    4160.181915  127.071574   3917.759039        30364.878807   
3         3    5942.464021  258.735026   3867.264053        22316.068069   
4         4    6818.312296  404.383551   1543.796329        32572.337282   

   competitor_count  household_income  parking_slots  staff_count  \
0                 2        601.025317              9         37.0   
1                 4        534.899105             15         37.0   
2                 6        418.976512             11         15.0   
3                 5        437.702779             13         27.0   
4                 1        428.428072             12         42.0   

   business_hours  building_age  signboard_size  
0              18     24.089964       18.060646  
1              16     12.604745       27.993712  
2              24      9.910629       27.624726  
3              18     18.884532       23.163064  
4              24      7.727617       25.940754  

 

このデータセットは、150店に関する立地特性、店舗属性、その他要因と、月間売上で構成されるデータです。

  • 行数: 150(店舗数)
  • 列数: 11(目的変数1、説明変数10)

目的変数は、月間売上(monthly_sales)です。

説明変数は以下の10変数です。

  • 立地特性
    • pop_density: 周辺人口密度(persons/km²)
    • station_passengers: 最寄駅の1日当たり乗降客数(persons/day)
    • competitor_count: 半径1km以内の競合店数(件)
    • household_income: 商圏の世帯年収(10k JPY単位)
  • 店舗属性
    • sales_area: 売場面積(m²)
    • parking_slots: 駐車場台数(台)
    • staff_count: スタッフ数(人)
    • business_hours: 営業時間(hours:16/18/24のカテゴリ)
  • その他
    • building_age: 築年数(years)
    • signboard_size: 看板サイズ(m²)

 

 基本統計量の確認

各変数の平均・標準偏差・最小値・最大値などの基本統計量を確認し、データの「顔」を数値で把握します。

以下、コードです。

print("【基本統計量】")
print(df.drop(columns=['store_id']).describe().round(1))

以下、実行結果です。

【基本統計量】
       monthly_sales  sales_area  pop_density  station_passengers  \
count          150.0       150.0        150.0               150.0   
mean          7362.5       306.7       7394.7             40549.0   
std           1737.0       113.3       4205.1             20807.3   
min           3751.0       102.8        519.6              6069.2   
25%           5947.3       210.6       3921.3             22510.5   
50%           7418.9       314.2       6864.6             41037.2   
75%           8599.5       398.6      11420.5             56651.5   
max          11784.3       498.5      14774.5             79366.5   

       competitor_count  household_income  parking_slots  staff_count  \
count             150.0             150.0          150.0        150.0   
mean                3.1             497.7           14.7         32.6   
std                 1.7              93.5            3.8         11.4   
min                 0.0             276.9            7.0         11.0   
25%                 2.0             431.7           12.0         23.0   
50%                 3.0             502.1           15.0         33.0   
75%                 4.0             560.9           17.0         42.0   
max                 8.0             733.1           28.0         54.0   

       business_hours  building_age  signboard_size  
count           150.0         150.0           150.0  
mean             19.6          12.3            18.0  
std               3.2           7.1             7.5  
min              16.0           0.2             5.0  
25%              18.0           7.0            11.2  
50%              18.0          12.5            17.9  
75%              24.0          18.2            24.4  
max              24.0          24.9            29.9  

 

月間売上の平均や範囲を確認できます。

売上は概ね数千万円程度のオーダーで、変数によってスケールが大きく異なることがわかります。

人口密度は数千〜数万、競合店数は0〜10程度と、桁がまったく違います。

この「スケールの違い」は、後のステップで標準化偏回帰係数を使って「影響度の比較」を行う際に重要になります。

 

探索的データ分析(EDA)

回帰モデルを構築する前に、データの全体像を可視化して理解しておくことは不可欠です。

データ分析は「まず目で見る」ことから始まります。

 

 売上と各変数の関係を散布図で見る

売上と各説明変数の関係を散布図で確認します。

各散布図の上に相関係数(r)を表示することで、売上との関連の強さを数値でも把握します。

以下、コードです。

# 5行×2列のサブプロットを作成
fig, axes = plt.subplots(5, 2, figsize=(10, 20))  
# 2次元配列のaxesを1次元に平坦化
axes = axes.flatten()  
# 目的変数とIDを除いた説明変数
explanatory_vars = [
    c for c in df.columns 
    if c not in ['monthly_sales', 'store_id']
] 

# 各説明変数についてループ
for i, col in enumerate(explanatory_vars):  
    # 説明変数 vs 月間売上の散布図
    axes[i].scatter(
        df[col], df['monthly_sales'], 
        alpha=0.5, s=30, c='#2E86AB'
    )
    axes[i].set_xlabel(col, fontsize=10)
    axes[i].set_ylabel('monthly_sales' if i % 5 == 0 else '')
    # 相関係数を計算してタイトルに表示
    r = df[col].corr(df['monthly_sales'])
    axes[i].set_title(f'r = {r:.3f}', fontsize=10)

plt.tight_layout()
plt.show()

 

以下、実行結果です。

 

売場面積(sales_area)や人口密度(pop_density)など、売上(monthly_sales)と正の相関を持つ変数が確認できます。

一方で、看板サイズ(signboard_size)や築年数(building_age)のように相関がほとんど見られない変数もあります。

ただし、ここで大事な注意点があります。相関係数はあくまで単純な二変量の関係であり、他の変数の影響を考慮していません。

たとえば「最寄駅の乗降客数」が売上と相関しているように見えても、それは「乗降客数が多い=人口密度が高い」という間接的な関係かもしれません。

重回帰分析を行うことで、他の変数を制御した上での「純粋な効果」がわかります。

 

 相関行列で変数どうしの関係を見る

変数間の相関をヒートマップで可視化します。

ここでは目的変数(売上)との相関だけでなく、説明変数どうしの相関にも注目します。

説明変数間の強い相関は、後で説明する「多重共線性」の予兆となるためです。

以下、コードです。

plt.figure(figsize=(10, 8))
# 相関行列の計算
corr = df.corr()
# 上三角をマスク
mask = np.triu(np.ones_like(corr, dtype=bool))  
# ヒートマップの描画
sns.heatmap(
    corr, mask=mask, annot=True, cmap='RdBu_r',
    center=0, fmt='.2f', square=True, linewidths=0.5
)
plt.title('変数間の相関行列', fontsize=14)
plt.tight_layout()
plt.show()

 

以下、実行結果です。

 

売上との相関が強い変数と弱い変数が一目でわかります。

同時に、説明変数同士の相関もチェックしてみましょう。

もし説明変数間の相関が0.8を超えるようなペアがあれば、多重共線性に注意が必要です。

今回の場合ですと、売場面積(sales_area)とスタッフ数(staff_count)の相関係数が約0.99と非常に高いことが分かります。

 

全変数を投入した重回帰分析

 重回帰分析

まずは手元にあるすべての説明変数を投入した重回帰モデルを構築します。

これが「ベースラインモデル」となり、後のステップで変数を絞り込んだモデルと比較する基準になります。

重回帰分析のモデルは、p 個の説明変数 x_1, x_2, \ldots, x_p を使って目的変数 y を予測する式で表されます。

$$
y = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_p x_p + \varepsilon
$$

ここで \beta_0 は切片、\beta_j は各説明変数の偏回帰係数、\varepsilon は誤差項です。

偏回帰係数 \beta_j は「他の変数を一定に保ったとき、x_j が1単位増えると y がどれだけ変化するか」を表しています。

散布図で見た単純相関とは異なり、他の変数の影響を取り除いた「純粋な効果」を示すのが偏回帰係数の強みです。

 

 モデルの全体的な性能を確認する

statsmodelsを使って全変数モデルを構築し、R^2(決定係数)と自由度調整済みR^2を確認します。

この2つの指標の差が、モデルに不要な変数が含まれているかどうかの最初のヒントになります。

以下、コードです。

# 説明変数Xと目的変数yを設定
X = df.drop(['monthly_sales','store_id'], axis=1)
y = df['monthly_sales']

# statsmodelsで回帰分析(定数項を追加して実行)
X_const = sm.add_constant(X)
model_full = sm.OLS(y, X_const).fit()

print("【全変数モデルの結果(抜粋)】")
print(f"R²: {model_full.rsquared:.4f}")
print(f"自由度調整済みR²: {model_full.rsquared_adj:.4f}")
print(
    f"F統計量: {model_full.fvalue:.2f} "
    f"(p値: {model_full.f_pvalue:.2e})"
)

 

以下、実行結果です。

【全変数モデルの結果(抜粋)】
R²: 0.9724
自由度調整済みR²: 0.9704
F統計量: 490.19 (p値: 4.14e-103)

 

R^2 と自由度調整済み R^2 の差に注目してください。

変数を増やせば R^2 は必ず上昇しますが、自由度調整済み R^2 は不要な変数が入ると下がります。

両者の差が大きい場合は、モデルに不要な変数が含まれている可能性を示唆しています。

今回は、F統計量のp値が十分小さいため(通常0.05未満)、「少なくともいずれかの説明変数は売上と関連がある」ことが統計的に確認できます。

 

 各変数の偏回帰係数とp値を読む

各変数の偏回帰係数・標準誤差・p値を一覧で確認し、どの変数が統計的に有意かを判断します。

p値の横に★印を表示して読みやすくしています。

以下、コードです。

# 結果のデータフレームを作成
results = pd.DataFrame({
    '偏回帰係数': model_full.params, 
    '標準誤差': model_full.bse, 
    'p値': model_full.pvalues, 
}).drop('const')  # 切片(const)は除外

# 有意性のマークを付与(p値に応じて***, **, *)
results['有意性'] = results['p値'].apply(
    lambda p: '***' if p < 0.001 else
              ('**' if p < 0.01 else
               ('*' if p < 0.05 else ''))
)

# 結果の表示
print("【偏回帰係数と有意性】")
print(results.round(4).to_string()) 
print("\n有意水準: *** p<0.001, ** p<0.01, * p<0.05")

 

以下、実行結果です。

【偏回帰係数と有意性】
                    偏回帰係数  標準誤差    p値   有意性
sales_area            3.7699   1.6518  0.0240    *
pop_density           0.3028   0.0061  0.0000  ***
station_passengers    0.0006   0.0012  0.6224     
competitor_count   -138.5469  14.3432  0.0000  ***
household_income      2.9941   0.2658  0.0000  ***
parking_slots        31.0289   6.5698  0.0000  ***
staff_count          51.5733  16.3244  0.0019   **
business_hours       -0.1502   7.8533  0.9848     
building_age          1.0473   3.5468  0.7682     
signboard_size        0.8034   3.3482  0.8107     

 

p値が0.05未満の変数は統計的に有意、つまり「売上への影響が偶然では説明できない」と判断できます。

一方、p値が大きい変数(看板サイズ、営業時間など)は、このモデルでは売上との有意な関係が認められません。

ここで重要な注意点が2つあります。ひとつは、「p値が大きい=影響がない」と即断するのは危険だということです。

サンプルサイズが小さい場合や多重共線性がある場合は、本来重要な変数のp値が大きくなることもあります。

もうひとつは、偏回帰係数の「大きさ」を変数間でそのまま比較してはいけないということです。

売場面積の係数が8で競合店数の係数が-150だからといって、競合店数の方が影響が大きいとは限りません。

変数のスケールが違うためです。影響度の公平な比較は、後編で標準化偏回帰係数を使って行います。

 

多重共線性の診断

 多重共線性とは何か

複数の説明変数を使う重回帰分析では、多重共線性(multicollinearity)のチェックが不可欠です。

多重共線性とは、説明変数の間に強い相関がある状態のことで、これが起きると偏回帰係数が不安定になり、推定結果が信頼できなくなります。

たとえば「売場面積」と「スタッフ数」が非常に強い相関を持つ場合を考えてみましょう。

売上増加の原因が「面積が広いから」なのか「スタッフが多いから」なのか、モデルが区別できなくなります。

結果として、係数がデータのわずかな変化で大きくブレてしまい、「面積を増やすと売上が上がる」という結論と「面積を増やすと売上が下がる」という結論が、データセットの微妙な違いで入れ替わるようなことが起きます。

 

 VIF(分散拡大係数)で診断する

多重共線性の程度を数値で評価するのがVIF(Variance Inflation Factor、分散拡大係数)です。

ある説明変数 x_j のVIFは、x_j を他のすべての説明変数で回帰したときの R^2_j を使って次のように計算されます。
$$
\text{VIF}_j = \frac{1}{1 – R^2_j}
$$

VIFが高いほど、その変数は他の変数で「説明できてしまう」ことを意味します。

つまり、その変数が持つ情報の多くが他の変数と重複しているということです。

  • VIF < 5 → 問題なし
  • 5 ≦ VIF < 10 → 注意
  • 10 ≦ VIF → 要対処

 

各変数のVIFを計算し、多重共線性の程度を確認します。

以下、コードです。

# 空のデータフレームを作成
vif_data = pd.DataFrame()
# 変数名をセット
vif_data['variable'] = X.columns

# 各変数のVIFを計算
vif_data['VIF'] = [
    variance_inflation_factor(X.values, i)
    for i in range(X.shape[1])
]

print("【VIF(分散拡大係数)】")
print(
    vif_data.sort_values('VIF', ascending=False)
    .round(2).to_string(index=False)
)

 

以下、実行結果です。

【VIF(分散拡大係数)】
          variable    VIF
       staff_count 533.23
        sales_area 490.09
    business_hours  28.26
  household_income  23.17
     parking_slots  13.52
    signboard_size   6.48
station_passengers   5.05
       pop_density   4.45
  competitor_count   4.18
      building_age   3.93

 

VIFがすべて5未満であれば、深刻な多重共線性はないと判断できます。

もしVIFが10を超える変数があった場合は、その変数を除外するか、PCA(第1回・第2回で学んだ手法)で次元削減を行ってから回帰分析を行う方法(主成分回帰)が有効です。

今回のデータでは、スタッフ数(staff_count)および売場面積(sales_area)、営業時間(business_hours)および商圏の世帯年収(household_income)、駐車場台数(parking_slots)のVIFが高い値になっています。

 

ここまでの振り返り

ここで一度立ち止まって、全変数モデルの結果を振り返りましょう。

全変数モデルのサマリーから、統計的に有意な変数と有意でない変数を整理して、現状の問題点を明確にします。

以下、コードです。

# 有意な変数と有意でない変数を分類
sig_vars = results[results['p値'] < 0.05].index.tolist() nonsig_vars = results[results['p値'] >= 0.05].index.tolist()

print("【全変数モデルの診断まとめ】")
print(f"\nモデル全体の説明力:")
print(f"  R² = {model_full.rsquared:.4f}")
print(f"  自由度調整済みR² = {model_full.rsquared_adj:.4f}")
print(f"  差分 = {model_full.rsquared - model_full.rsquared_adj:.4f}")

print(f"\n統計的に有意な変数(p < 0.05): {len(sig_vars)}個")
for v in sig_vars:
    coef = results.loc[v, '偏回帰係数']
    p = results.loc[v, 'p値']
    print(f"  {v}: 係数={coef:.4f}, p値={p:.4f}")

print(f"\n有意でない変数(p ≥ 0.05): {len(nonsig_vars)}個")
for v in nonsig_vars:
    p = results.loc[v, 'p値']
    print(f"  {v}: p値={p:.4f}")

print(f"\n→ {len(nonsig_vars)}個の変数はモデルから除外できる可能性があります")

 

以下、実行結果です。

【全変数モデルの診断まとめ】

モデル全体の説明力:
  R² = 0.9724
  自由度調整済みR² = 0.9704
  差分 = 0.0020

統計的に有意な変数(p < 0.05): 6個
  sales_area: 係数=3.7699, p値=0.0240
  pop_density: 係数=0.3028, p値=0.0000
  competitor_count: 係数=-138.5469, p値=0.0000
  household_income: 係数=2.9941, p値=0.0000
  parking_slots: 係数=31.0289, p値=0.0000
  staff_count: 係数=51.5733, p値=0.0019

有意でない変数(p ≥ 0.05): 4個
  station_passengers: p値=0.6224
  business_hours: p値=0.9848
  building_age: p値=0.7682
  signboard_size: p値=0.8107

→ 4個の変数はモデルから除外できる可能性があります

 

全変数モデルの現状が整理されます。

R^2 と自由度調整済み R^2 の差、有意な変数と有意でない変数の一覧が出力されるので、「どの変数がモデルに貢献していて、どの変数がノイズになっている可能性があるか」が明確になります。

全変数モデルの問題点を整理すると、次の3つに集約されます。

  • 不要な変数が含まれている:p値が大きい変数はモデルの精度向上に貢献しておらず、むしろ過学習のリスクを高めています。
  • 偏回帰係数の比較が難しい:変数のスケールが異なるため、係数の大きさをそのまま比較して「どの要因が最も影響が大きいか」を判断できません。
  • 本当に新しいデータで予測できるかわからない:手元のデータへの当てはまり(R^2)が良くても、見たことのないデータに対する予測力は別問題です。

これらの問題を解決するのが、次回(後編)で扱うLasso回帰による変数選択標準化偏回帰係数による影響度比較交差検証によるモデル評価です。

 

まとめ

今回は、ドラッグストアチェーンの店舗データを題材に、以下の分析を実施しました。

ステップ やったこと 得られた知見
データの理解 基本統計量の確認 変数間のスケールの大きな違い
EDA(散布図) 売上と各変数の散布図 + 相関係数 売上との関連が強い変数・弱い変数の把握
EDA(相関行列) 変数間の相関ヒートマップ 説明変数間の冗長性の確認
重回帰分析 全変数モデルの構築 R²、各変数のp値、有意な変数の特定
多重共線性診断 VIFの計算 変数間の共線性の程度を数値で確認

重要なのは、「全部入りモデル」をそのまま信じてはいけないという気づきです。

全変数を投入すると R^2 は高くなりますが、不要な変数が含まれている可能性があり、新しいデータに対する予測力は保証されません。

p値も、多重共線性やサンプルサイズの影響を受けます。

次回(後編)では、Lasso回帰で本当に効く変数だけを自動選択し、標準化偏回帰係数で影響度を公平に比較し、交差検証でモデルの安定性を確認します。

そして最後に、新規出店候補地の売上予測と経営層向けのビジネスレポート作成まで一気に進めます。