Lecture 1データサイエンスとは — 全体像とPython環境構築

8:00

データサイエンスとは — 全体像とPython環境構築

この講義で学ぶこと

データサイエンスは、統計学・プログラミング・ドメイン知識を組み合わせて、データから価値ある洞察を引き出す学問分野です。本講義では、データサイエンスの全体像を把握し、実際に手を動かすための Python 環境を構築します。

データサイエンスとは何か

データサイエンスは3つの領域が交差する分野です。

領域 内容 具体例
統計学・数学 データの分布や関係性を定量的に分析 平均・分散、回帰分析、仮説検定
プログラミング 大量データの処理・自動化 Python、SQL、データパイプライン
ドメイン知識 業界固有の知識と文脈理解 マーケティング、金融、医療

データサイエンスが解決する課題

  • 記述的分析:何が起きたか?(売上レポート、ダッシュボード)
  • 診断的分析:なぜ起きたか?(原因分析、相関分析)
  • 予測的分析:何が起きるか?(需要予測、離脱予測)
  • 処方的分析:何をすべきか?(レコメンド、最適化)

CRISP-DM:データサイエンスの標準プロセス

実務で最も広く使われるフレームワークが CRISP-DM(Cross-Industry Standard Process for Data Mining)です。

1. ビジネス理解Business Understanding
    解決したい課題を明確にする
2. データ理解Data Understanding
    利用可能なデータを探索把握する
3. データ準備Data Preparation
    分析に適した形にデータを加工する
4. モデリングModeling
    機械学習モデルを構築チューニングする
5. 評価Evaluation
    モデルの性能をビジネス観点で評価する
6. 展開Deployment
    モデルを実運用に組み込む

各フェーズは順番に進むだけでなく、前のフェーズに戻って反復することが一般的です。

Python環境の構築

Anacondaのインストール

Anaconda は、データサイエンスに必要なライブラリが最初からまとめてインストールされるディストリビューションです。

# Anacondaのダウンロード(公式サイトから)
# https://www.anaconda.com/download

# インストール後、バージョン確認
conda --version
python --version

pip を使ったインストール(軽量環境向け)

# 仮想環境の作成
python -m venv ds-env

# 仮想環境の有効化(Windows)
ds-env\Scripts\activate

# 仮想環境の有効化(Mac/Linux)
source ds-env/bin/activate

# 主要ライブラリのインストール
pip install pandas numpy matplotlib seaborn scikit-learn jupyter

Jupyter Notebook の起動と基本操作

# Jupyter Notebook を起動
jupyter notebook

Jupyter Notebook はセル単位でコードを実行できる対話型の開発環境です。

# 最初のセル:ライブラリのインポート確認
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_iris

print(f"pandas: {pd.__version__}")
print(f"numpy: {np.__version__}")
print(f"seaborn: {sns.__version__}")

# 動作確認:irisデータセットの読み込み
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['species'] = iris.target
print(f"\nデータの形状: {df.shape}")
print(df.head())

主要ライブラリの役割

ライブラリ 役割 本コースでの使用場面
pandas データ操作・分析 データの読み込み・加工・集計
NumPy 数値計算 配列演算・数学関数
matplotlib グラフ描画(基盤) 折れ線・棒・散布図
seaborn 統計的可視化 ヒートマップ・ペアプロット
scikit-learn 機械学習 モデル構築・評価
SciPy 科学技術計算 統計検定・最適化

データサイエンティストのキャリアパス

データサイエンスに関わる職種は多様化しています。

職種 主な業務 重点スキル
データアナリスト BI・レポート・可視化 SQL、Excel、Tableau
データサイエンティスト 予測モデル・仮説検証 Python、統計、ML
MLエンジニア モデルの本番運用 Docker、API、MLOps
データエンジニア データ基盤構築 SQL、Spark、Airflow

実践ワーク

以下の手順で環境構築を完了させましょう。

  1. Anaconda または pip で Python 環境を構築する
  2. Jupyter Notebook を起動して新しいノートブックを作成する
  3. 上記のライブラリインポートコードを実行し、全てエラーなく動作することを確認する
  4. iris データセットを読み込み、df.describe() で基本統計量を確認する
# 実践ワーク:基本統計量の確認
import pandas as pd
from sklearn.datasets import load_iris

iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['species'] = iris.target

# 基本統計量
print(df.describe())

# データ型の確認
print(df.dtypes)

# 各種カウント
print(f"\n行数: {len(df)}, 列数: {len(df.columns)}")
print(f"欠損値の数:\n{df.isnull().sum()}")

まとめと次回の準備

  • データサイエンスは統計・プログラミング・ドメイン知識の融合分野である
  • CRISP-DMは反復的なプロセスで、ビジネス理解から展開まで6段階ある
  • Python環境は Anaconda または pip + venv で構築でき、Jupyter Notebook で対話的に分析できる
  • 主要ライブラリ(pandas, NumPy, matplotlib, seaborn, scikit-learn)の役割を把握した
  • 次回は pandas を使ったデータ操作の基本を学ぶ。CSV ファイルの読み込みや DataFrame の操作に進む

参考文献

Lecture 2pandas入門 — DataFrame操作の基本

12:00

pandas入門 — DataFrame操作の基本

この講義で学ぶこと

pandas は Python のデータ分析において最も重要なライブラリです。本講義では、pandas の中核である DataFrame の作成・読み込み・基本操作を体系的に学びます。

DataFrameとは

DataFrame は行と列からなる2次元のデータ構造で、Excel のスプレッドシートやSQLテーブルに似た形式でデータを扱えます。

import pandas as pd
import numpy as np

# 辞書からDataFrameを作成
data = {
    '名前': ['田中太郎', '鈴木花子', '佐藤一郎', '高橋美咲', '山田健太'],
    '年齢': [28, 34, 45, 23, 31],
    '部署': ['営業', 'エンジニア', 'マーケティング', 'エンジニア', '営業'],
    '年収(万円)': [450, 620, 580, 400, 510],
    '入社年': [2020, 2015, 2008, 2023, 2018]
}
df = pd.DataFrame(data)
print(df)

出力:

     名前  年齢         部署  年収(万円)  入社年
0  田中太郎   28         営業       450  2020
1  鈴木花子   34    エンジニア       620  2015
2  佐藤一郎   45  マーケティング       580  2008
3  高橋美咲   23    エンジニア       400  2023
4  山田健太   31         営業       510  2018

CSVファイルの読み込み

実務では CSV ファイルからデータを読み込むケースが大半です。

# CSVファイルの読み込み
# df = pd.read_csv('sales_data.csv')

# サンプルデータで代用(CSVファイルがない場合)
import io

csv_data = """日付,商品名,カテゴリ,数量,単価,売上
2025-01-05,ノートPC,電子機器,3,89000,267000
2025-01-05,マウス,周辺機器,10,2500,25000
2025-01-06,モニター,電子機器,2,45000,90000
2025-01-06,キーボード,周辺機器,8,5000,40000
2025-01-07,ノートPC,電子機器,1,89000,89000
2025-01-07,ヘッドセット,周辺機器,5,8000,40000
2025-01-08,モニター,電子機器,4,45000,180000
2025-01-08,マウス,周辺機器,15,2500,37500
"""

df = pd.read_csv(io.StringIO(csv_data))
print(df)

read_csv の便利なオプション

パラメータ 説明 使用例
encoding 文字コード指定 encoding='shift_jis'
header ヘッダー行の指定 header=0(1行目)
index_col インデックスにする列 index_col='日付'
parse_dates 日付として解析する列 parse_dates=['日付']
usecols 読み込む列を指定 usecols=['名前','年齢']
nrows 読み込む行数の制限 nrows=100
dtype データ型の指定 dtype={'ID': str}
# 日付解析付きで読み込む例
df = pd.read_csv(io.StringIO(csv_data), parse_dates=['日付'])
print(df.dtypes)

データの基本確認メソッド

データを読み込んだら、まず全体像を把握します。

# 先頭5行を確認
print(df.head())

# 末尾3行を確認
print(df.tail(3))

# データの形状(行数, 列数)
print(f"形状: {df.shape}")

# データ型・欠損値の情報
print(df.info())

# 数値列の基本統計量
print(df.describe())

head / tail / info / describe の使い分け

メソッド 目的 確認できること
head(n) 先頭n行を表示 データの構造・値のイメージ
tail(n) 末尾n行を表示 データの終端・ソート確認
info() 列情報の一覧 列名・型・非null数・メモリ使用量
describe() 基本統計量 count, mean, std, min, 25%, 50%, 75%, max
shape 行数と列数 データ規模の把握
dtypes 各列のデータ型 int64, float64, object, datetime64
columns 列名一覧 全列名のリスト

列の選択とフィルタリング

列の選択

# 1列の選択(Seriesが返る)
names = df['商品名']
print(names)

# 複数列の選択(DataFrameが返る)
subset = df[['商品名', '数量', '売上']]
print(subset)

行のフィルタリング(条件抽出)

# 単一条件:売上が10万円以上
high_sales = df[df['売上'] >= 100000]
print("売上10万以上:")
print(high_sales)

# 複数条件(AND):電子機器かつ数量2以上
filtered = df[(df['カテゴリ'] == '電子機器') & (df['数量'] >= 2)]
print("\n電子機器かつ数量2以上:")
print(filtered)

# 複数条件(OR):ノートPCまたはモニター
products = df[(df['商品名'] == 'ノートPC') | (df['商品名'] == 'モニター')]
print("\nノートPCまたはモニター:")
print(products)

# isinを使った複数値マッチ
selected = df[df['商品名'].isin(['ノートPC', 'モニター'])]
print("\nisinを使った選択:")
print(selected)

ソートと集計

# 売上で降順ソート
sorted_df = df.sort_values('売上', ascending=False)
print("売上降順:")
print(sorted_df)

# 複数キーでソート
sorted_multi = df.sort_values(['カテゴリ', '売上'], ascending=[True, False])
print("\nカテゴリ昇順→売上降順:")
print(sorted_multi)

# 基本的な集計
print(f"\n売上合計: {df['売上'].sum():,}円")
print(f"売上平均: {df['売上'].mean():,.0f}円")
print(f"売上最大: {df['売上'].max():,}円")
print(f"売上最小: {df['売上'].min():,}円")
print(f"取引件数: {df['売上'].count()}")

列の追加と変換

# 新しい列の追加
df['利益率'] = 0.3  # 全行に同じ値
df['利益'] = df['売上'] * df['利益率']
print(df[['商品名', '売上', '利益率', '利益']])

# 条件に基づく列の作成
df['売上ランク'] = np.where(df['売上'] >= 100000, '高', '低')
print(df[['商品名', '売上', '売上ランク']])

インデックス操作

# インデックスのリセット
df_reset = df.reset_index(drop=True)

# 特定の列をインデックスに設定
df_indexed = df.set_index('商品名')
print(df_indexed)

# locによるラベルベースのアクセス
print(df_indexed.loc['ノートPC'])

# ilocによる位置ベースのアクセス
print(df.iloc[0:3, 0:3])  # 先頭3行、先頭3列

loc と iloc の違い

アクセサ 指定方法
loc ラベル(名前) df.loc[0:2, '商品名':'数量']
iloc 位置(整数) df.iloc[0:3, 0:3]

実践ワーク

以下の課題に取り組んでみましょう。

# 課題用データの作成
import pandas as pd
import numpy as np

np.random.seed(42)
n = 50
employees = pd.DataFrame({
    'ID': range(1, n+1),
    '名前': [f'社員{i:03d}' for i in range(1, n+1)],
    '部署': np.random.choice(['営業', 'エンジニア', 'マーケティング', '人事', '経理'], n),
    '年齢': np.random.randint(22, 60, n),
    '年収': np.random.randint(300, 1000, n) * 10000,
    '勤続年数': np.random.randint(0, 30, n),
    '評価スコア': np.round(np.random.uniform(1.0, 5.0, n), 1)
})

# 課題1: エンジニア部署の社員だけを抽出し、年収の降順で表示
engineers = employees[employees['部署'] == 'エンジニア'].sort_values('年収', ascending=False)
print("課題1: エンジニアの年収降順")
print(engineers[['名前', '年収', '評価スコア']])

# 課題2: 年齢30歳以上かつ評価スコア4.0以上の社員を抽出
top_performers = employees[(employees['年齢'] >= 30) & (employees['評価スコア'] >= 4.0)]
print(f"\n課題2: 30歳以上かつ評価4.0以上 → {len(top_performers)}名")

# 課題3: 部署ごとの平均年収を計算
dept_salary = employees.groupby('部署')['年収'].mean()
print(f"\n課題3: 部署別平均年収")
print(dept_salary.sort_values(ascending=False))

まとめと次回の準備

  • DataFrame は pandas の中核で、行と列からなる2次元のデータ構造である
  • read_csv() でCSVファイルを読み込み、head(), info(), describe() でデータ全体像を把握する
  • 列選択df['列名'] または df[['列A','列B']] で行い、行フィルタはブール条件 df[条件] で行う
  • sort_values() でソート、sum() / mean() / max() で集計ができる
  • loc(ラベル)と iloc(位置)の使い分けを理解した
  • 次回はデータの前処理として、欠損値処理・型変換・重複削除を学ぶ

参考文献

Lecture 3データの前処理 — 欠損値・型変換・重複削除

12:00

データの前処理 — 欠損値・型変換・重複削除

この講義で学ぶこと

実務のデータは「汚い」状態で届くのが普通です。欠損値、不正な型、重複行など、分析の障害となるデータ品質の問題を体系的に処理する方法を学びます。前処理はデータサイエンスの作業時間の約60〜80%を占めると言われており、最も重要なスキルの一つです。

サンプルデータの準備

まず、実務で遭遇する典型的な「汚い」データを作成します。

import pandas as pd
import numpy as np

# 問題のあるサンプルデータ
data = {
    '顧客ID': ['C001', 'C002', 'C003', 'C004', 'C005', 'C002', 'C006', 'C007', 'C008', 'C003'],
    '名前': ['田中太郎', '鈴木花子', '佐藤一郎', None, '山田健太', '鈴木花子', '高橋美咲', '伊藤次郎', '渡辺恵', '佐藤一郎'],
    '年齢': [28, 34, np.nan, 23, 45, 34, None, 31, 29, np.nan],
    '性別': ['男', '女', '男', '女', '男', '女', '女', '男', '女', '男'],
    '購入金額': ['15000', '32000', '8500', '24000', np.nan, '32000', '19000', '45000', 'N/A', '8500'],
    '登録日': ['2024-01-15', '2024/02/20', '2024-03-10', '24-04-05', '2024-05-22', '2024/02/20', '2024-06-01', None, '2024-07-15', '2024-03-10'],
    '都道府県': ['東京都', '大阪府', '東京都', '愛知県', '福岡県 ', '大阪府', ' 北海道', '東京都', '神奈川県', '東京都']
}
df = pd.DataFrame(data)
print("元データ:")
print(df)
print(f"\nデータ形状: {df.shape}")
print(f"\nデータ型:\n{df.dtypes}")

欠損値の検出と処理

欠損値の確認

# 欠損値の確認
print("欠損値の数:")
print(df.isnull().sum())
print(f"\n全体の欠損率:")
print((df.isnull().sum() / len(df) * 100).round(1))

# 欠損値のあるパターンを可視化
print("\n欠損値のパターン:")
print(df.isnull().astype(int))

欠損値の処理方法

方法 関数 適用場面
行削除 dropna() 欠損が少なく、データが十分にある場合
値補完(固定値) fillna(値) 明確なデフォルト値がある場合
値補完(平均値) fillna(df['列'].mean()) 数値データで分布に偏りがない場合
値補完(中央値) fillna(df['列'].median()) 外れ値がある数値データ
値補完(最頻値) fillna(df['列'].mode()[0]) カテゴリデータ
前方補完 fillna(method='ffill') 時系列データ
# 年齢の欠損値を中央値で補完
age_median = df['年齢'].median()
df['年齢'] = df['年齢'].fillna(age_median)
print(f"年齢の欠損値を中央値({age_median})で補完:")
print(df['年齢'])

# 名前の欠損値がある行を確認
print(f"\n名前が欠損している行:")
print(df[df['名前'].isnull()])

# 名前の欠損値は「不明」で補完
df['名前'] = df['名前'].fillna('不明')

データ型の変換

文字列から数値への変換

# 購入金額が文字列型になっている
print(f"購入金額の型: {df['購入金額'].dtype}")
print(f"購入金額のユニーク値: {df['購入金額'].unique()}")

# 'N/A'を欠損値として扱いながら数値に変換
df['購入金額'] = pd.to_numeric(df['購入金額'], errors='coerce')
print(f"\n変換後の型: {df['購入金額'].dtype}")
print(df['購入金額'])

# 欠損値(変換できなかった値)を平均値で補完
mean_price = df['購入金額'].mean()
df['購入金額'] = df['購入金額'].fillna(mean_price)
print(f"\n購入金額の欠損値を平均値({mean_price:.0f})で補完:")
print(df['購入金額'])

astype による型変換

# 年齢を整数型に変換
df['年齢'] = df['年齢'].astype(int)

# 型の確認
print("変換後のデータ型:")
print(df.dtypes)

日付型への変換

# 様々な形式の日付を統一的に変換
df['登録日'] = pd.to_datetime(df['登録日'], errors='coerce', format='mixed')
print("日付変換後:")
print(df['登録日'])

# 日付の欠損値を確認
print(f"\n登録日の欠損数: {df['登録日'].isnull().sum()}")

# 日付から年月を抽出
df['登録年月'] = df['登録日'].dt.strftime('%Y-%m')
print(df[['顧客ID', '登録日', '登録年月']])

重複データの検出と削除

# 重複行の確認
print(f"重複行の数: {df.duplicated().sum()}")

# 特定の列に基づく重複確認
print(f"\n顧客IDの重複: {df.duplicated(subset=['顧客ID']).sum()}")
print("\n重複している行:")
print(df[df.duplicated(subset=['顧客ID'], keep=False)])

# 重複の削除(最初の出現を残す)
df_unique = df.drop_duplicates(subset=['顧客ID'], keep='first')
print(f"\n重複削除後の行数: {len(df_unique)}(元: {len(df)})")
print(df_unique)

duplicated / drop_duplicates のオプション

パラメータ 説明
subset 重複判定の対象列 ['顧客ID'], ['名前','年齢']
keep どの重複行を残すか 'first'(最初), 'last'(最後), False(全削除)

文字列の前処理

# 前後の空白を除去
df_unique['都道府県'] = df_unique['都道府県'].str.strip()
print("空白除去後:")
print(df_unique['都道府県'])

# strアクセサを使った文字列操作
print("\n都道府県に'東京'を含む行:")
print(df_unique[df_unique['都道府県'].str.contains('東京', na=False)])

# 文字列の置換
df_unique['都道府県_短縮'] = df_unique['都道府県'].str.replace('都|府|県', '', regex=True)
print("\n短縮表記:")
print(df_unique[['都道府県', '都道府県_短縮']])

よく使う str メソッド

メソッド 機能
str.strip() 前後の空白削除 ' 東京 ''東京'
str.lower() 小文字変換 'Tokyo''tokyo'
str.upper() 大文字変換 'Tokyo''TOKYO'
str.contains() 部分一致検索 str.contains('東京')
str.replace() 文字列置換 str.replace('都', '')
str.len() 文字数取得 str.len()
str.split() 文字列分割 str.split('-')

apply と map による柔軟な変換

# applyで各要素に関数を適用
def categorize_age(age):
    if age < 25:
        return '若手'
    elif age < 35:
        return '中堅'
    elif age < 45:
        return 'ベテラン'
    else:
        return 'シニア'

df_unique['年齢カテゴリ'] = df_unique['年齢'].apply(categorize_age)
print("年齢カテゴリ:")
print(df_unique[['名前', '年齢', '年齢カテゴリ']])

# mapで値をマッピング
gender_map = {'男': 'Male', '女': 'Female'}
df_unique['性別_英語'] = df_unique['性別'].map(gender_map)
print("\n性別マッピング:")
print(df_unique[['名前', '性別', '性別_英語']])

前処理パイプラインの整理

実務では前処理の手順をまとめて関数化しておくと再利用が容易です。

def preprocess_customer_data(df):
    """顧客データの前処理パイプライン"""
    df = df.copy()

    # 1. 重複削除
    df = df.drop_duplicates(subset=['顧客ID'], keep='first')

    # 2. 欠損値処理
    df['名前'] = df['名前'].fillna('不明')
    df['年齢'] = pd.to_numeric(df['年齢'], errors='coerce')
    df['年齢'] = df['年齢'].fillna(df['年齢'].median()).astype(int)

    # 3. 型変換
    df['購入金額'] = pd.to_numeric(df['購入金額'], errors='coerce')
    df['購入金額'] = df['購入金額'].fillna(df['購入金額'].mean())
    df['登録日'] = pd.to_datetime(df['登録日'], errors='coerce', format='mixed')

    # 4. 文字列クリーニング
    df['都道府県'] = df['都道府県'].str.strip()

    return df

# パイプラインの実行
df_clean = preprocess_customer_data(pd.DataFrame(data))
print("前処理完了:")
print(df_clean.info())
print(df_clean)

実践ワーク

以下の「汚い」データを前処理してください。

import pandas as pd
import numpy as np

# 実践用データ
messy_data = pd.DataFrame({
    '商品コード': ['P001', 'P002', 'P003', 'P001', 'P004', 'P005', 'P002', 'P006'],
    '商品名': ['  りんご ', 'バナナ', None, 'りんご', 'みかん', 'ぶどう', 'バナナ', 'メロン '],
    '価格': ['150', '200', '300', '150', 'unknown', '500', '200', '800'],
    '在庫数': [50, 30, np.nan, 50, 20, None, 30, 10],
    '産地': ['青森', 'フィリピン', '山梨', '青森', '愛媛', '山梨', 'フィリピン', '北海道']
})

# 課題:
# 1. 重複行を商品コードに基づいて削除する
# 2. 商品名の前後の空白を除去し、欠損値を「未登録」で埋める
# 3. 価格を数値型に変換し、変換できなかった値を中央値で補完する
# 4. 在庫数の欠損値を平均値で補完し、整数型に変換する

# 解答例
df_work = messy_data.copy()
df_work = df_work.drop_duplicates(subset=['商品コード'], keep='first')
df_work['商品名'] = df_work['商品名'].str.strip().fillna('未登録')
df_work['価格'] = pd.to_numeric(df_work['価格'], errors='coerce')
df_work['価格'] = df_work['価格'].fillna(df_work['価格'].median())
df_work['在庫数'] = df_work['在庫数'].fillna(df_work['在庫数'].mean()).astype(int)

print("前処理後のデータ:")
print(df_work)
print(f"\nデータ型:\n{df_work.dtypes}")

まとめと次回の準備

  • 欠損値isnull() で検出し、dropna() / fillna() で処理する。補完方法は平均値・中央値・最頻値など場面に応じて選択する
  • 型変換pd.to_numeric(), pd.to_datetime(), astype() を使い分ける。errors='coerce' で変換不能な値を NaN に変換できる
  • 重複削除drop_duplicates() で行い、subsetkeep で挙動を制御する
  • 文字列操作str アクセサ(.str.strip(), .str.contains() など)で行う
  • apply()map() で柔軟なデータ変換を実現できる
  • 前処理は関数化してパイプラインにまとめると再利用しやすい
  • 次回はデータの可視化を学ぶ。matplotlib と seaborn でデータを視覚的に理解する方法に進む

参考文献

Lecture 4データの可視化 — matplotlib・seabornで伝わるグラフ

12:00

データの可視化 — matplotlib・seabornで伝わるグラフ

この講義で学ぶこと

データを視覚的に表現することは、分析結果を伝えるうえで最も強力な手段です。本講義では matplotlib と seaborn を使い、目的に応じたグラフの作成方法とスタイリングの基本を学びます。

サンプルデータの準備

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# 日本語フォントの設定(環境に応じて変更)
plt.rcParams['font.family'] = 'MS Gothic'  # Windows
# plt.rcParams['font.family'] = 'Hiragino Sans'  # Mac
plt.rcParams['axes.unicode_minus'] = False

# サンプル:月別売上データ
np.random.seed(42)
months = pd.date_range('2024-01', periods=12, freq='MS')
sales_data = pd.DataFrame({
    '月': months,
    'A商品': np.random.randint(100, 300, 12) * 1000,
    'B商品': np.random.randint(80, 250, 12) * 1000,
    'C商品': np.random.randint(50, 180, 12) * 1000
})
sales_data['月名'] = sales_data['月'].dt.strftime('%m月')

# サンプル:tipsデータセット(seaborn内蔵)
tips = sns.load_dataset('tips')
print(tips.head())
print(f"\ntipsデータの形状: {tips.shape}")

matplotlib の基本

折れ線グラフ(plt.plot)

時系列データのトレンドを表示するのに適しています。

fig, ax = plt.subplots(figsize=(10, 5))

ax.plot(sales_data['月名'], sales_data['A商品'], marker='o', label='A商品', linewidth=2)
ax.plot(sales_data['月名'], sales_data['B商品'], marker='s', label='B商品', linewidth=2)
ax.plot(sales_data['月名'], sales_data['C商品'], marker='^', label='C商品', linewidth=2)

ax.set_title('月別商品売上推移(2024年)', fontsize=14, fontweight='bold')
ax.set_xlabel('月', fontsize=12)
ax.set_ylabel('売上(円)', fontsize=12)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'{x/10000:.0f}万'))

plt.tight_layout()
plt.show()

棒グラフ(plt.bar)

カテゴリ間の比較に適しています。

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 縦棒グラフ
categories = ['営業', 'エンジニア', 'マーケ', '人事', '経理']
values = [45, 38, 28, 15, 12]
colors = ['#2196F3', '#4CAF50', '#FF9800', '#9C27B0', '#F44336']

axes[0].bar(categories, values, color=colors, edgecolor='white', linewidth=1.5)
axes[0].set_title('部署別人数', fontsize=13, fontweight='bold')
axes[0].set_ylabel('人数')

# 各バーに値を表示
for i, v in enumerate(values):
    axes[0].text(i, v + 0.5, str(v), ha='center', fontweight='bold')

# 横棒グラフ
axes[1].barh(categories, values, color=colors, edgecolor='white', linewidth=1.5)
axes[1].set_title('部署別人数(横棒)', fontsize=13, fontweight='bold')
axes[1].set_xlabel('人数')

plt.tight_layout()
plt.show()

散布図(plt.scatter)

2つの数値変数の関係性を調べるのに使います。

fig, ax = plt.subplots(figsize=(8, 6))

scatter = ax.scatter(
    tips['total_bill'], tips['tip'],
    c=tips['size'], cmap='viridis',
    alpha=0.7, s=60, edgecolors='white', linewidth=0.5
)

ax.set_title('合計金額 vs チップ額', fontsize=14, fontweight='bold')
ax.set_xlabel('合計金額(ドル)', fontsize=12)
ax.set_ylabel('チップ額(ドル)', fontsize=12)
plt.colorbar(scatter, label='人数')
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

ヒストグラム(plt.hist)

データの分布を把握するために使います。

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 基本的なヒストグラム
axes[0].hist(tips['total_bill'], bins=20, color='#2196F3', edgecolor='white', alpha=0.8)
axes[0].set_title('合計金額の分布', fontsize=13, fontweight='bold')
axes[0].set_xlabel('合計金額(ドル)')
axes[0].set_ylabel('頻度')
axes[0].axvline(tips['total_bill'].mean(), color='red', linestyle='--', label=f"平均: {tips['total_bill'].mean():.1f}")
axes[0].legend()

# 重ね合わせヒストグラム
for day in ['Thur', 'Fri', 'Sat', 'Sun']:
    subset = tips[tips['day'] == day]['total_bill']
    axes[1].hist(subset, bins=15, alpha=0.5, label=day)

axes[1].set_title('曜日別 合計金額の分布', fontsize=13, fontweight='bold')
axes[1].set_xlabel('合計金額(ドル)')
axes[1].set_ylabel('頻度')
axes[1].legend()

plt.tight_layout()
plt.show()

subplots によるレイアウト

fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# 左上:折れ線
axes[0, 0].plot(sales_data['月名'], sales_data['A商品'], 'o-', color='#2196F3')
axes[0, 0].set_title('A商品 月別売上')
axes[0, 0].tick_params(axis='x', rotation=45)

# 右上:棒グラフ
total_by_product = [sales_data['A商品'].sum(), sales_data['B商品'].sum(), sales_data['C商品'].sum()]
axes[0, 1].bar(['A商品', 'B商品', 'C商品'], total_by_product, color=['#2196F3', '#4CAF50', '#FF9800'])
axes[0, 1].set_title('商品別年間売上合計')

# 左下:ヒストグラム
axes[1, 0].hist(tips['tip'], bins=20, color='#4CAF50', edgecolor='white')
axes[1, 0].set_title('チップ額の分布')

# 右下:散布図
axes[1, 1].scatter(tips['total_bill'], tips['tip'], alpha=0.5, color='#FF9800')
axes[1, 1].set_title('合計金額 vs チップ')

plt.suptitle('データ可視化の例', fontsize=16, fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()

seaborn による統計的可視化

seaborn は matplotlib をベースに、美しく統計的に意味のあるグラフを簡単に作成できるライブラリです。

ヒートマップ(sns.heatmap)

相関係数の可視化に最適です。

# 相関行列のヒートマップ
fig, ax = plt.subplots(figsize=(8, 6))

corr_matrix = tips[['total_bill', 'tip', 'size']].corr()
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', center=0,
            fmt='.2f', square=True, linewidths=1, ax=ax)
ax.set_title('相関係数ヒートマップ', fontsize=14, fontweight='bold')

plt.tight_layout()
plt.show()

ペアプロット(sns.pairplot)

変数間の全組み合わせを一覧できます。

# irisデータでペアプロット
from sklearn.datasets import load_iris

iris = load_iris()
iris_df = pd.DataFrame(iris.data, columns=['がく片長', 'がく片幅', '花弁長', '花弁幅'])
iris_df['品種'] = pd.Categorical.from_codes(iris.target, iris.target_names)

g = sns.pairplot(iris_df, hue='品種', height=2.5, diag_kind='kde',
                 plot_kws={'alpha': 0.6, 's': 40})
g.figure.suptitle('Iris データセット ペアプロット', y=1.02, fontsize=14)
plt.show()

箱ひげ図(sns.boxplot)

分布の比較と外れ値の検出に有効です。

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 曜日別の合計金額
sns.boxplot(data=tips, x='day', y='total_bill', palette='Set2',
            order=['Thur', 'Fri', 'Sat', 'Sun'], ax=axes[0])
axes[0].set_title('曜日別 合計金額の分布', fontsize=13, fontweight='bold')
axes[0].set_xlabel('曜日')
axes[0].set_ylabel('合計金額(ドル)')

# 性別×喫煙の組み合わせ
sns.boxplot(data=tips, x='day', y='tip', hue='sex', palette='Set1',
            order=['Thur', 'Fri', 'Sat', 'Sun'], ax=axes[1])
axes[1].set_title('曜日×性別別 チップの分布', fontsize=13, fontweight='bold')
axes[1].set_xlabel('曜日')
axes[1].set_ylabel('チップ(ドル)')

plt.tight_layout()
plt.show()

バイオリンプロット・カウントプロット

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# バイオリンプロット
sns.violinplot(data=tips, x='day', y='total_bill', palette='muted',
               order=['Thur', 'Fri', 'Sat', 'Sun'], ax=axes[0])
axes[0].set_title('曜日別 合計金額(バイオリン)', fontsize=13)

# カウントプロット
sns.countplot(data=tips, x='day', hue='time', palette='pastel',
              order=['Thur', 'Fri', 'Sat', 'Sun'], ax=axes[1])
axes[1].set_title('曜日×時間帯の件数', fontsize=13)

plt.tight_layout()
plt.show()

グラフの用途別選択ガイド

目的 推奨グラフ 関数
時系列トレンド 折れ線グラフ plt.plot()
カテゴリ比較 棒グラフ plt.bar() / sns.barplot()
2変数の関係 散布図 plt.scatter() / sns.scatterplot()
分布の確認 ヒストグラム plt.hist() / sns.histplot()
分布の比較 箱ひげ図 sns.boxplot()
相関の全体像 ヒートマップ sns.heatmap()
多変量の関係 ペアプロット sns.pairplot()
構成比 円グラフ plt.pie()
カテゴリ件数 カウントプロット sns.countplot()

実践ワーク

seaborn の tips データセットを使って、以下のグラフを作成してください。

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

tips = sns.load_dataset('tips')
plt.rcParams['font.family'] = 'MS Gothic'

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 課題1: 合計金額のヒストグラム(昼食 vs 夕食で色分け)
sns.histplot(data=tips, x='total_bill', hue='time', kde=True,
             palette='Set1', alpha=0.5, ax=axes[0, 0])
axes[0, 0].set_title('時間帯別 合計金額の分布')

# 課題2: 曜日別の平均チップ額(棒グラフ)
sns.barplot(data=tips, x='day', y='tip', palette='Set2',
            order=['Thur', 'Fri', 'Sat', 'Sun'], ax=axes[0, 1])
axes[0, 1].set_title('曜日別 平均チップ額')

# 課題3: 合計金額 vs チップの散布図(喫煙者で色分け)
sns.scatterplot(data=tips, x='total_bill', y='tip', hue='smoker',
                style='smoker', palette='Set1', alpha=0.7, ax=axes[1, 0])
axes[1, 0].set_title('合計金額 vs チップ(喫煙有無)')

# 課題4: 性別×時間帯別の合計金額(箱ひげ図)
sns.boxplot(data=tips, x='sex', y='total_bill', hue='time',
            palette='pastel', ax=axes[1, 1])
axes[1, 1].set_title('性別×時間帯別 合計金額')

plt.suptitle('tipsデータセット 実践ワーク', fontsize=16, fontweight='bold')
plt.tight_layout()
plt.show()

まとめと次回の準備

  • matplotlib は Python の可視化の基盤であり、plt.plot(), plt.bar(), plt.scatter(), plt.hist() が基本4種のグラフ
  • subplots で複数のグラフを1つの図にまとめてレイアウトできる
  • seaborn は統計的な可視化に特化しており、heatmap, pairplot, boxplot が特に強力
  • グラフには必ずタイトル・軸ラベル・凡例を付けて、一目で内容が伝わるようにする
  • 目的に応じた適切なグラフの種類を選択することが重要
  • 次回は探索的データ分析(EDA)を学ぶ。データから仮説を立て、分析の方向性を見定める方法に進む

参考文献

Lecture 5探索的データ分析(EDA) — データから仮説を立てる

12:00

探索的データ分析(EDA) — データから仮説を立てる

この講義で学ぶこと

探索的データ分析(Exploratory Data Analysis、EDA)は、モデル構築の前にデータの特性を深く理解するプロセスです。統計量の確認、分布の把握、変数間の関係性の発見を通じて、分析の仮説を立てます。

EDAのワークフロー

1. データの全体像を把握する
    shape, info, describe, head
2. 各変数の分布を確認する
    ヒストグラム, value_counts, ユニーク数
3. 変数間の関係を調べる
    相関行列, 散布図, groupby
4. 外れ値を検出する
    IQR法, 箱ひげ図
5. 仮説を立てる
    発見した傾向から分析の方向性を決める

サンプルデータの準備

実際のデータ分析に近い、従業員データセットを作成して EDA を実践します。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

plt.rcParams['font.family'] = 'MS Gothic'
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42)

# 従業員データの作成(200名分)
n = 200
departments = np.random.choice(['営業', 'エンジニア', 'マーケティング', '人事', '経理'], n, p=[0.3, 0.25, 0.2, 0.15, 0.1])
experience = np.random.randint(0, 25, n)
base_salary = 300 + experience * 20 + np.random.normal(0, 50, n)
dept_bonus = {'営業': 30, 'エンジニア': 80, 'マーケティング': 20, '人事': 0, '経理': 10}
salary = base_salary + np.array([dept_bonus[d] for d in departments])
salary = np.clip(salary, 280, 1200)

employees = pd.DataFrame({
    '社員ID': [f'EMP{i:04d}' for i in range(1, n+1)],
    '部署': departments,
    '年齢': np.clip(22 + experience + np.random.randint(-2, 3, n), 22, 65),
    '勤続年数': experience,
    '年収(万円)': np.round(salary, 0).astype(int),
    '評価スコア': np.round(np.clip(np.random.normal(3.5, 0.8, n), 1.0, 5.0), 1),
    '残業時間(月平均)': np.clip(np.random.exponential(15, n), 0, 80).astype(int),
    '離職フラグ': np.random.choice([0, 1], n, p=[0.85, 0.15])
})

print(f"データ形状: {employees.shape}")
print(employees.head(10))

ステップ1:全体像の把握

# 基本情報
print("=== データ情報 ===")
print(employees.info())

print("\n=== 基本統計量 ===")
print(employees.describe())

print("\n=== 欠損値 ===")
print(employees.isnull().sum())

print("\n=== カテゴリ変数の確認 ===")
print(f"部署のユニーク値: {employees['部署'].nunique()}")
print(employees['部署'].value_counts())

ステップ2:各変数の分布確認

数値変数の分布

numerical_cols = ['年齢', '勤続年数', '年収(万円)', '評価スコア', '残業時間(月平均)']

fig, axes = plt.subplots(2, 3, figsize=(16, 10))
axes = axes.flatten()

for i, col in enumerate(numerical_cols):
    axes[i].hist(employees[col], bins=25, color='#2196F3', edgecolor='white', alpha=0.8)
    axes[i].axvline(employees[col].mean(), color='red', linestyle='--',
                    label=f'平均: {employees[col].mean():.1f}')
    axes[i].axvline(employees[col].median(), color='green', linestyle='-.',
                    label=f'中央値: {employees[col].median():.1f}')
    axes[i].set_title(f'{col} の分布', fontsize=12)
    axes[i].legend(fontsize=9)

axes[5].set_visible(False)
plt.suptitle('数値変数の分布確認', fontsize=15, fontweight='bold')
plt.tight_layout()
plt.show()

カテゴリ変数の分布(value_counts)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 部署の分布
dept_counts = employees['部署'].value_counts()
axes[0].bar(dept_counts.index, dept_counts.values, color='#4CAF50', edgecolor='white')
axes[0].set_title('部署別人数', fontsize=13)
for i, v in enumerate(dept_counts.values):
    axes[0].text(i, v + 1, str(v), ha='center', fontweight='bold')

# 離職フラグの分布
turnover_counts = employees['離職フラグ'].value_counts()
labels = ['在籍', '離職']
axes[1].pie(turnover_counts.values, labels=labels, autopct='%1.1f%%',
            colors=['#2196F3', '#F44336'], startangle=90)
axes[1].set_title('離職率', fontsize=13)

plt.tight_layout()
plt.show()

ステップ3:変数間の関係性

groupby による集計

# 部署別の集計
dept_stats = employees.groupby('部署').agg({
    '年収(万円)': ['mean', 'median', 'std'],
    '評価スコア': 'mean',
    '残業時間(月平均)': 'mean',
    '離職フラグ': 'mean'
}).round(1)
print("部署別集計:")
print(dept_stats)

# 離職者 vs 在籍者の比較
turnover_comparison = employees.groupby('離職フラグ').agg({
    '年収(万円)': 'mean',
    '評価スコア': 'mean',
    '残業時間(月平均)': 'mean',
    '勤続年数': 'mean'
}).round(1)
turnover_comparison.index = ['在籍', '離職']
print("\n離職者 vs 在籍者:")
print(turnover_comparison)

相関行列

# 数値変数の相関行列
fig, ax = plt.subplots(figsize=(10, 8))

corr = employees[numerical_cols + ['離職フラグ']].corr()
mask = np.triu(np.ones_like(corr, dtype=bool))

sns.heatmap(corr, annot=True, cmap='RdBu_r', center=0, fmt='.2f',
            mask=mask, square=True, linewidths=1, ax=ax,
            vmin=-1, vmax=1)
ax.set_title('変数間の相関行列', fontsize=14, fontweight='bold')

plt.tight_layout()
plt.show()

# 強い相関の抽出
print("相関係数が0.3以上のペア:")
for i in range(len(corr.columns)):
    for j in range(i+1, len(corr.columns)):
        if abs(corr.iloc[i, j]) >= 0.3:
            print(f"  {corr.columns[i]} × {corr.columns[j]}: {corr.iloc[i, j]:.3f}")

クロス集計

# 部署 × 離職の関係
cross_tab = pd.crosstab(employees['部署'], employees['離職フラグ'], margins=True)
cross_tab.columns = ['在籍', '離職', '合計']
cross_tab['離職率(%)'] = (cross_tab['離職'] / cross_tab['合計'] * 100).round(1)
print("部署別離職率:")
print(cross_tab)

ステップ4:外れ値の検出

IQR法による外れ値検出

四分位範囲(IQR = Q3 - Q1)を使い、Q1 - 1.5IQR 未満または Q3 + 1.5IQR を超える値を外れ値とします。

def detect_outliers_iqr(df, column):
    """IQR法による外れ値検出"""
    Q1 = df[column].quantile(0.25)
    Q3 = df[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR

    outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
    return outliers, lower_bound, upper_bound

# 各数値変数の外れ値を検出
print("=== 外れ値検出(IQR法) ===")
for col in numerical_cols:
    outliers, lb, ub = detect_outliers_iqr(employees, col)
    if len(outliers) > 0:
        print(f"\n{col}: {len(outliers)}件の外れ値(範囲: {lb:.1f} ~ {ub:.1f})")

箱ひげ図による外れ値の可視化

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# 部署別年収の箱ひげ図
sns.boxplot(data=employees, x='部署', y='年収(万円)', palette='Set2', ax=axes[0])
axes[0].set_title('部署別年収の分布', fontsize=12)

# 部署別残業時間
sns.boxplot(data=employees, x='部署', y='残業時間(月平均)', palette='Set3', ax=axes[1])
axes[1].set_title('部署別残業時間の分布', fontsize=12)

# 離職別評価スコア
sns.boxplot(data=employees, x='離職フラグ', y='評価スコア', palette='Set1', ax=axes[2])
axes[2].set_xticklabels(['在籍', '離職'])
axes[2].set_title('離職有無別 評価スコア', fontsize=12)

plt.tight_layout()
plt.show()

ステップ5:仮説の立案

EDA の結果から仮説を立てます。

# 散布図で関係性を可視化
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# 仮説1: 残業時間が多いと離職しやすいのでは?
sns.boxplot(data=employees, x='離職フラグ', y='残業時間(月平均)', palette='Set1', ax=axes[0])
axes[0].set_xticklabels(['在籍', '離職'])
axes[0].set_title('仮説1: 残業時間 → 離職', fontsize=12)

# 仮説2: 年収が低いと離職しやすいのでは?
sns.boxplot(data=employees, x='離職フラグ', y='年収(万円)', palette='Set1', ax=axes[1])
axes[1].set_xticklabels(['在籍', '離職'])
axes[1].set_title('仮説2: 年収 → 離職', fontsize=12)

# 仮説3: 勤続年数と年収に正の相関があるのでは?
sns.scatterplot(data=employees, x='勤続年数', y='年収(万円)', hue='部署',
                alpha=0.6, ax=axes[2])
axes[2].set_title('仮説3: 勤続年数 vs 年収', fontsize=12)

plt.tight_layout()
plt.show()

実践ワーク

以下のデータセットで完全な EDA を行ってください。

# 実践用:ECサイトの購入データ
np.random.seed(123)
n = 300
ec_data = pd.DataFrame({
    '顧客ID': [f'C{i:04d}' for i in range(1, n+1)],
    '年齢': np.random.randint(18, 65, n),
    '性別': np.random.choice(['男性', '女性'], n),
    '会員ランク': np.random.choice(['一般', 'シルバー', 'ゴールド', 'プラチナ'], n, p=[0.5, 0.25, 0.15, 0.1]),
    '購入回数': np.random.poisson(5, n),
    '購入金額合計': np.random.exponential(15000, n).astype(int),
    '最終購入からの日数': np.random.exponential(30, n).astype(int),
    'クーポン使用率(%)': np.clip(np.random.normal(40, 20, n), 0, 100).round(1),
    'リピート購入': np.random.choice([0, 1], n, p=[0.4, 0.6])
})

# 課題: 以下を実施してください
# 1. describe() で全体像を把握する
print(ec_data.describe())

# 2. 会員ランク別の平均購入金額・購入回数を集計する
print(ec_data.groupby('会員ランク')[['購入金額合計', '購入回数']].mean().round(0))

# 3. 相関行列を作成し、リピート購入に関係する変数を見つける
print(ec_data.select_dtypes(include=[np.number]).corr()['リピート購入'].sort_values(ascending=False))

# 4. 外れ値を IQR法 で検出する
for col in ['購入金額合計', '購入回数', '最終購入からの日数']:
    outliers, lb, ub = detect_outliers_iqr(ec_data, col)
    print(f"{col}: 外れ値 {len(outliers)}件")

まとめと次回の準備

  • EDA はモデル構築前の最重要ステップで、データの特性を深く理解するプロセス
  • 5ステップで体系的に進める:全体像 → 分布 → 関係性 → 外れ値 → 仮説
  • describe(), value_counts(), groupby() が EDA の三大メソッド
  • 相関行列corr() + sns.heatmap())で変数間の線形関係を一覧できる
  • IQR法で外れ値を定量的に検出し、箱ひげ図で視覚的に確認する
  • EDA の最終成果は仮説の立案であり、次の分析ステップの方向性を決定する
  • 次回は統計の基礎を学ぶ。平均・分散・相関・仮説検定の理論的背景を理解する

参考文献

Lecture 6統計の基礎 — 平均・分散・相関・仮説検定

12:00

統計の基礎 — 平均・分散・相関・仮説検定

この講義で学ぶこと

データサイエンスの根幹は統計学です。本講義では、データの要約(記述統計)、変数間の関係(相関)、そして差や効果の有意性を判定する(仮説検定)ための基礎理論と Python 実装を学びます。

記述統計:データを数値で要約する

代表値(中心傾向の指標)

指標 定義 特徴 使い分け
平均(mean) 全値の合計 / データ数 外れ値に敏感 正規分布に近いデータ
中央値(median) ソートした中央の値 外れ値に頑健 歪んだ分布のデータ
最頻値(mode) 最も頻度が高い値 カテゴリデータにも使える カテゴリデータ・離散データ
import pandas as pd
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
import seaborn as sns

plt.rcParams['font.family'] = 'MS Gothic'
plt.rcParams['axes.unicode_minus'] = False

# サンプルデータ:社員の月収(万円)
np.random.seed(42)
salaries_normal = np.random.normal(40, 8, 100)  # 正規分布に近い
salaries_skewed = np.concatenate([np.random.normal(35, 5, 90), np.random.normal(80, 10, 10)])  # 右に歪んだ分布

for name, data in [('正規分布型', salaries_normal), ('歪んだ分布', salaries_skewed)]:
    print(f"\n=== {name} ===")
    print(f"  平均:   {np.mean(data):.1f}万円")
    print(f"  中央値: {np.median(data):.1f}万円")
    print(f"  最頻値: {stats.mode(data, keepdims=True).mode[0]:.1f}万円")
    print(f"  → 平均と中央値の差: {abs(np.mean(data) - np.median(data)):.1f}万円")

散布度(ばらつきの指標)

# 分散と標準偏差
data = np.array([45, 38, 52, 41, 49, 35, 58, 42, 47, 43])

variance = np.var(data, ddof=1)      # 不偏分散(ddof=1)
std_dev = np.std(data, ddof=1)       # 標準偏差
data_range = np.ptp(data)            # 範囲(最大-最小)
iqr = np.percentile(data, 75) - np.percentile(data, 25)  # 四分位範囲

print(f"データ: {data}")
print(f"分散:       {variance:.2f}")
print(f"標準偏差:   {std_dev:.2f}")
print(f"範囲:       {data_range}")
print(f"四分位範囲: {iqr:.2f}")
print(f"変動係数:   {std_dev / np.mean(data) * 100:.1f}%")
指標 定義 特徴
分散(variance) 各値と平均の差の二乗の平均 単位が元データの二乗
標準偏差(std) 分散の平方根 元データと同じ単位で解釈しやすい
範囲(range) 最大値 - 最小値 外れ値に影響されやすい
四分位範囲(IQR) Q3 - Q1 外れ値に頑健
変動係数(CV) 標準偏差 / 平均 単位の異なるデータの比較に使う

正規分布

正規分布はデータサイエンスの多くの手法の前提となる最も重要な分布です。

from scipy.stats import norm

fig, ax = plt.subplots(figsize=(10, 5))

x = np.linspace(-4, 4, 1000)
y = norm.pdf(x, 0, 1)

ax.plot(x, y, 'b-', linewidth=2, label='標準正規分布 N(0,1)')
ax.fill_between(x, y, where=(x >= -1) & (x <= 1), alpha=0.3, color='blue', label='±1σ (68.3%)')
ax.fill_between(x, y, where=((x >= -2) & (x < -1)) | ((x > 1) & (x <= 2)), alpha=0.2, color='green', label='±2σ (95.4%)')
ax.fill_between(x, y, where=((x >= -3) & (x < -2)) | ((x > 2) & (x <= 3)), alpha=0.1, color='red', label='±3σ (99.7%)')

ax.set_title('正規分布と標準偏差の関係', fontsize=14, fontweight='bold')
ax.set_xlabel('標準偏差(σ)', fontsize=12)
ax.set_ylabel('確率密度', fontsize=12)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# 正規分布に従うかの確認(シャピロ・ウィルク検定)
stat, p_value = stats.shapiro(salaries_normal)
print(f"正規分布型データ: 統計量={stat:.4f}, p値={p_value:.4f} → {'正規分布に従う' if p_value > 0.05 else '正規分布ではない'}")

stat, p_value = stats.shapiro(salaries_skewed)
print(f"歪んだデータ:     統計量={stat:.4f}, p値={p_value:.4f} → {'正規分布に従う' if p_value > 0.05 else '正規分布ではない'}")

相関分析

ピアソン相関係数

2つの数値変数間の線形関係の強さを -1 から 1 の範囲で表します。

# サンプルデータ
np.random.seed(42)
n = 100
study_hours = np.random.uniform(1, 10, n)
test_score = 30 + 6 * study_hours + np.random.normal(0, 8, n)

# 相関係数の計算
r, p_value = stats.pearsonr(study_hours, test_score)
print(f"ピアソン相関係数: r = {r:.3f}")
print(f"p値: {p_value:.2e}")
print(f"解釈: {'統計的に有意' if p_value < 0.05 else '有意でない'}")

# 可視化
fig, ax = plt.subplots(figsize=(8, 6))
ax.scatter(study_hours, test_score, alpha=0.5, color='#2196F3')

# 回帰直線を追加
z = np.polyfit(study_hours, test_score, 1)
p = np.poly1d(z)
x_line = np.linspace(study_hours.min(), study_hours.max(), 100)
ax.plot(x_line, p(x_line), 'r--', linewidth=2, label=f'回帰直線 (r={r:.3f})')

ax.set_title('学習時間 vs テストスコア', fontsize=14, fontweight='bold')
ax.set_xlabel('学習時間(時間)', fontsize=12)
ax.set_ylabel('テストスコア(点)', fontsize=12)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

相関係数の解釈基準

| 相関係数 (|r|) | 強さの解釈 | |----------------|-----------| | 0.00 ~ 0.19 | ほぼ無相関 | | 0.20 ~ 0.39 | 弱い相関 | | 0.40 ~ 0.59 | 中程度の相関 | | 0.60 ~ 0.79 | 強い相関 | | 0.80 ~ 1.00 | 非常に強い相関 |

注意点:相関関係は因果関係ではない。アイスクリームの売上と水難事故は正の相関があるが、アイスクリームが水難事故を起こすわけではない(気温という交絡因子がある)。

スピアマン順位相関係数

非線形な関係や順序データに適しています。

# 非線形な関係のあるデータ
x_nonlinear = np.random.uniform(0, 5, 100)
y_nonlinear = x_nonlinear ** 2 + np.random.normal(0, 2, 100)

r_pearson, _ = stats.pearsonr(x_nonlinear, y_nonlinear)
r_spearman, _ = stats.spearmanr(x_nonlinear, y_nonlinear)

print(f"ピアソン相関係数:   r = {r_pearson:.3f}(線形関係のみ検出)")
print(f"スピアマン相関係数: r = {r_spearman:.3f}(単調な関係も検出)")

仮説検定の基礎

検定の流れ

1. 帰無仮説H₀と対立仮説H₁を設定する
2. 有意水準αを決める通常 0.05
3. 検定統計量を計算する
4. p値を求める
5. p値 < α なら帰無仮説を棄却  統計的に有意

t検定:2群の平均の比較

# 2つのグループの比較:新旧教材の効果
np.random.seed(42)
old_method = np.random.normal(65, 12, 50)  # 旧教材の成績
new_method = np.random.normal(72, 10, 50)  # 新教材の成績

# 対応のないt検定(独立2標本)
t_stat, p_value = stats.ttest_ind(old_method, new_method)

print("=== t検定結果 ===")
print(f"旧教材: 平均={old_method.mean():.1f}, 標準偏差={old_method.std():.1f}")
print(f"新教材: 平均={new_method.mean():.1f}, 標準偏差={new_method.std():.1f}")
print(f"t統計量: {t_stat:.3f}")
print(f"p値: {p_value:.4f}")
print(f"結論: {'有意な差がある(H₀棄却)' if p_value < 0.05 else '有意な差がない(H₀採用)'}")

# 可視化
fig, ax = plt.subplots(figsize=(8, 5))
ax.hist(old_method, bins=15, alpha=0.5, color='#F44336', label=f'旧教材 (μ={old_method.mean():.1f})')
ax.hist(new_method, bins=15, alpha=0.5, color='#2196F3', label=f'新教材 (μ={new_method.mean():.1f})')
ax.set_title(f't検定: p={p_value:.4f}', fontsize=14, fontweight='bold')
ax.set_xlabel('テストスコア', fontsize=12)
ax.set_ylabel('頻度', fontsize=12)
ax.legend(fontsize=11)

plt.tight_layout()
plt.show()

カイ二乗検定:カテゴリ変数の独立性

# 性別と商品カテゴリの関係を検定
observed = pd.DataFrame({
    '食品': [120, 90],
    '家電': [80, 110],
    '衣類': [100, 100]
}, index=['男性', '女性'])

chi2, p_value, dof, expected = stats.chi2_contingency(observed)

print("=== カイ二乗検定 ===")
print("観測度数:")
print(observed)
print(f"\n期待度数:")
print(pd.DataFrame(expected, columns=observed.columns, index=observed.index).round(1))
print(f"\nカイ二乗統計量: {chi2:.3f}")
print(f"自由度: {dof}")
print(f"p値: {p_value:.4f}")
print(f"結論: {'性別と商品カテゴリに関連あり' if p_value < 0.05 else '関連なし'}")

p値の解釈ガイド

p値の範囲 慣例的な表記 解釈
p < 0.001 *** 非常に強い有意性
p < 0.01 ** 強い有意性
p < 0.05 * 有意
p >= 0.05 ns 有意でない

実践ワーク

# 実践:A/Bテストの分析
np.random.seed(99)

# ECサイトのA/Bテストデータ
group_a = np.random.normal(3200, 800, 200)   # 既存デザイン:購入金額
group_b = np.random.normal(3500, 900, 200)   # 新デザイン:購入金額

# 課題1: 両グループの記述統計を計算
print("=== 課題1: 記述統計 ===")
for name, data in [('グループA(既存)', group_a), ('グループB(新規)', group_b)]:
    print(f"{name}: 平均={data.mean():.0f}, 中央値={np.median(data):.0f}, 標準偏差={data.std():.0f}")

# 課題2: t検定で有意差を確認
t_stat, p_value = stats.ttest_ind(group_a, group_b)
print(f"\n=== 課題2: t検定 ===")
print(f"t統計量={t_stat:.3f}, p値={p_value:.4f}")
print(f"結論: {'新デザインに有意な効果あり' if p_value < 0.05 else '有意な差なし'}")

# 課題3: 効果量(Cohen's d)を計算
pooled_std = np.sqrt((group_a.std()**2 + group_b.std()**2) / 2)
cohens_d = (group_b.mean() - group_a.mean()) / pooled_std
print(f"\n=== 課題3: 効果量 ===")
print(f"Cohen's d = {cohens_d:.3f}")
print(f"効果の大きさ: {'小' if abs(cohens_d) < 0.5 else '中' if abs(cohens_d) < 0.8 else '大'}")

まとめと次回の準備

  • 代表値は平均・中央値・最頻値の3つがあり、データの分布に応じて使い分ける
  • 散布度は標準偏差が最も一般的で、データのばらつきを元の単位で表現する
  • 正規分布は多くの統計手法の前提であり、±1σに68.3%、±2σに95.4%のデータが含まれる
  • 相関係数は -1 から 1 の値で線形関係の強さを表し、因果関係とは区別する
  • 仮説検定は帰無仮説を立てて p 値で判定する。p < 0.05 で「統計的に有意」と判断する
  • 次回は機械学習の基礎を学ぶ。教師あり学習の仕組みと評価方法の概念に進む

参考文献

Lecture 7機械学習の基礎 — 教師あり学習の仕組み

12:00

機械学習の基礎 — 教師あり学習の仕組み

この講義で学ぶこと

機械学習はデータからパターンを自動的に学習し、新しいデータに対して予測を行う技術です。本講義では、機械学習の分類体系、教師あり学習の基本的な仕組み、そしてモデルの評価方法を学びます。

機械学習の分類

種類 定義 目的 代表的な手法
教師あり学習 正解ラベル付きデータで学習 予測 線形回帰、ロジスティック回帰、決定木、SVM
教師なし学習 正解ラベルなしで学習 構造発見 k-means、主成分分析(PCA)、階層クラスタリング
強化学習 試行錯誤で最適行動を学習 最適化 Q学習、方策勾配法

教師あり学習の2大タスク

タスク 目的変数の型 評価指標
回帰(Regression) 連続値(数値) 住宅価格予測、売上予測 MSE, RMSE, R²
分類(Classification) カテゴリ スパム判定、離脱予測 精度、適合率、再現率、F1

教師あり学習の仕組み

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris, make_regression
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, mean_squared_error

plt.rcParams['font.family'] = 'MS Gothic'
plt.rcParams['axes.unicode_minus'] = False

学習の流れ

1. データの準備
    特徴量Xと目的変数yに分ける
2. データの分割
    訓練データ70-80%)とテストデータ20-30%)
3. モデルの学習
    訓練データでモデルを構築fit
4. 予測
    テストデータで予測predict
5. 評価
    予測と正解を比較して性能を測る
# 教師あり学習の基本フロー(回帰の例)
X, y = make_regression(n_samples=200, n_features=1, noise=20, random_state=42)

# 1. データの分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
print(f"訓練データ: {X_train.shape[0]}件")
print(f"テストデータ: {X_test.shape[0]}件")

# 2. モデルの学習
model = LinearRegression()
model.fit(X_train, y_train)  # 訓練データで学習

# 3. 予測
y_pred = model.predict(X_test)  # テストデータで予測

# 4. 評価
mse = mean_squared_error(y_test, y_pred)
print(f"平均二乗誤差(MSE): {mse:.2f}")
print(f"二乗平均平方根誤差(RMSE): {np.sqrt(mse):.2f}")

# 可視化
fig, ax = plt.subplots(figsize=(8, 6))
ax.scatter(X_test, y_test, alpha=0.6, color='#2196F3', label='実際の値')
ax.scatter(X_test, y_pred, alpha=0.6, color='#F44336', marker='x', label='予測値')

# 回帰直線
x_line = np.linspace(X.min(), X.max(), 100).reshape(-1, 1)
ax.plot(x_line, model.predict(x_line), 'g--', linewidth=2, label='回帰直線')

ax.set_title('線形回帰の予測結果', fontsize=14, fontweight='bold')
ax.set_xlabel('特徴量 X', fontsize=12)
ax.set_ylabel('目的変数 y', fontsize=12)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

訓練データとテストデータの分割

なぜ分割が必要か?

モデルの性能は「未知のデータに対する予測力」で評価する必要があります。訓練データと同じデータで評価すると、モデルが丸暗記しているだけでも高スコアが出てしまいます。

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier

# Irisデータの準備
iris = load_iris()
X, y = iris.data, iris.target

# 分割なし(悪い例)
model_bad = DecisionTreeClassifier(random_state=42)
model_bad.fit(X, y)
score_train_only = accuracy_score(y, model_bad.predict(X))
print(f"訓練データで評価(分割なし): {score_train_only:.3f}")  # 過度に高い

# 適切な分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
model_good = DecisionTreeClassifier(random_state=42)
model_good.fit(X_train, y_train)
score_train = accuracy_score(y_train, model_good.predict(X_train))
score_test = accuracy_score(y_test, model_good.predict(X_test))
print(f"訓練データでの正解率: {score_train:.3f}")
print(f"テストデータでの正解率: {score_test:.3f}")

train_test_split のパラメータ

パラメータ 説明 推奨値
test_size テストデータの割合 0.2 ~ 0.3
random_state 乱数シード(再現性のため) 任意の整数
stratify 層化抽出(クラス比率を維持) 分類タスクでは y を指定
shuffle シャッフルするか True(デフォルト)
# 層化抽出(分類でクラスの比率を保つ)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

# クラス比率の確認
print("全データのクラス比率:", np.bincount(y) / len(y))
print("訓練データのクラス比率:", np.bincount(y_train) / len(y_train))
print("テストデータのクラス比率:", np.bincount(y_test) / len(y_test))

過学習と未学習

過学習(Overfitting)

訓練データに特化しすぎて、新しいデータに対する予測力が落ちる現象です。

未学習(Underfitting)

モデルが単純すぎて、訓練データのパターンすら捉えられていない状態です。

from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline

# 非線形なデータを生成
np.random.seed(42)
X_demo = np.sort(np.random.uniform(0, 1, 30)).reshape(-1, 1)
y_demo = np.sin(2 * np.pi * X_demo).ravel() + np.random.normal(0, 0.2, 30)

fig, axes = plt.subplots(1, 3, figsize=(18, 5))
x_plot = np.linspace(0, 1, 100).reshape(-1, 1)

degrees = [1, 4, 15]
titles = ['未学習(degree=1)', '適切(degree=4)', '過学習(degree=15)']

for ax, degree, title in zip(axes, degrees, titles):
    model = make_pipeline(PolynomialFeatures(degree), LinearRegression())
    model.fit(X_demo, y_demo)
    y_plot = model.predict(x_plot)

    ax.scatter(X_demo, y_demo, color='#2196F3', alpha=0.7, label='データ')
    ax.plot(x_plot, y_plot, 'r-', linewidth=2, label=f'モデル (degree={degree})')
    ax.plot(x_plot, np.sin(2 * np.pi * x_plot), 'g--', alpha=0.5, label='真の関数')
    ax.set_title(title, fontsize=13, fontweight='bold')
    ax.set_ylim(-2, 2)
    ax.legend(fontsize=9)
    ax.grid(True, alpha=0.3)

plt.suptitle('未学習 vs 適切 vs 過学習', fontsize=15, fontweight='bold')
plt.tight_layout()
plt.show()

対策まとめ

問題 症状 対策
過学習 訓練スコア高、テストスコア低 正則化、特徴量削減、データ増加
未学習 両方のスコアが低い より複雑なモデル、特徴量追加

交差検証(Cross-Validation)

データの分割に依存しない、より信頼性の高い評価方法です。

from sklearn.model_selection import cross_val_score

# 5分割交差検証
model = DecisionTreeClassifier(random_state=42)
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')

print("=== 5分割交差検証 ===")
print(f"各分割のスコア: {scores}")
print(f"平均スコア: {scores.mean():.3f}")
print(f"標準偏差: {scores.std():.3f}")
print(f"95%信頼区間: {scores.mean():.3f} ± {scores.std() * 2:.3f}")
交差検証の仕組み(5分割の場合):

  分割1: [テスト] [訓練] [訓練] [訓練] [訓練] → スコア1
  分割2: [訓練] [テスト] [訓練] [訓練] [訓練] → スコア2
  分割3: [訓練] [訓練] [テスト] [訓練] [訓練] → スコア3
  分割4: [訓練] [訓練] [訓練] [テスト] [訓練] → スコア4
  分割5: [訓練] [訓練] [訓練] [訓練] [テスト] → スコア5

  最終スコア = 5つのスコアの平均

評価指標の概要

回帰の評価指標

指標 数式概要 解釈
MSE (予測-実際)²の平均 小さいほど良い、外れ値に敏感
RMSE MSEの平方根 元データと同じ単位
MAE 予測-実際
1 - (残差/全分散) 1に近いほど良い(最大1)

分類の評価指標

指標 定義 解釈
正解率(Accuracy) 正解数 / 全体 全体的な正しさ
適合率(Precision) TP / (TP + FP) 陽性予測の信頼度
再現率(Recall) TP / (TP + FN) 実際の陽性の検出率
F1スコア 適合率と再現率の調和平均 バランスの取れた指標

実践ワーク

from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, classification_report

# Wineデータセット
wine = load_wine()
X, y = wine.data, wine.target
print(f"特徴量: {wine.feature_names}")
print(f"クラス: {wine.target_names}")
print(f"データ形状: {X.shape}")

# 課題1: データを70:30に分割(層化抽出あり)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

# 課題2: 決定木モデルで学習・予測
model = DecisionTreeClassifier(random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

# 課題3: 正解率を確認
print(f"\n訓練データの正解率: {accuracy_score(y_train, model.predict(X_train)):.3f}")
print(f"テストデータの正解率: {accuracy_score(y_test, y_pred):.3f}")

# 課題4: 5分割交差検証
cv_scores = cross_val_score(model, X, y, cv=5)
print(f"\n交差検証スコア: {cv_scores.mean():.3f} ± {cv_scores.std():.3f}")

# 分類レポート
print(f"\n分類レポート:")
print(classification_report(y_test, y_pred, target_names=wine.target_names))

まとめと次回の準備

  • 機械学習は教師あり学習(予測)、教師なし学習(構造発見)、強化学習(最適化)に分類される
  • 教師あり学習は回帰(数値予測)と分類(カテゴリ予測)の2大タスクがある
  • train_test_split() でデータを分割し、テストデータで「未知データへの予測力」を評価する
  • 過学習(訓練データに特化しすぎ)と未学習(モデルが単純すぎ)のバランスが重要
  • 交差検証cross_val_score())でデータ分割に依存しない安定した評価ができる
  • 次回は回帰分析として、線形回帰モデルの構築と評価指標の詳細を学ぶ

参考文献

Lecture 8回帰分析 — 数値を予測する

12:00

回帰分析 — 数値を予測する

この講義で学ぶこと

回帰分析は、入力データから連続的な数値を予測する手法です。本講義では、単回帰・重回帰の実装、特徴量選択、そして R², MSE, RMSE, MAE といった評価指標の使い方を体系的に学びます。

線形回帰のイメージ

線形回帰は「データに最もフィットする直線(超平面)を見つける」手法です。最小二乗法により、予測値と実際の値の差(残差)の二乗和を最小化します。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from sklearn.preprocessing import StandardScaler

plt.rcParams['font.family'] = 'MS Gothic'
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42)

単回帰分析

1つの特徴量から目的変数を予測します。

# サンプルデータ:広告費 vs 売上
n = 50
ad_spend = np.random.uniform(10, 100, n)  # 広告費(万円)
sales = 50 + 3.5 * ad_spend + np.random.normal(0, 30, n)  # 売上(万円)

df = pd.DataFrame({'広告費': ad_spend, '売上': sales})

# データの分割
X = df[['広告費']]  # 2次元配列にする
y = df['売上']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# モデルの学習
model = LinearRegression()
model.fit(X_train, y_train)

# 係数の確認
print(f"切片(intercept): {model.intercept_:.2f}")
print(f"傾き(coefficient): {model.coef_[0]:.2f}")
print(f"回帰式: 売上 = {model.intercept_:.2f} + {model.coef_[0]:.2f} × 広告費")

# 予測
y_pred = model.predict(X_test)

# 可視化
fig, ax = plt.subplots(figsize=(8, 6))
ax.scatter(X_train, y_train, alpha=0.6, color='#2196F3', label='訓練データ')
ax.scatter(X_test, y_test, alpha=0.6, color='#FF9800', marker='s', label='テストデータ')

x_line = np.linspace(X['広告費'].min(), X['広告費'].max(), 100).reshape(-1, 1)
ax.plot(x_line, model.predict(x_line), 'r-', linewidth=2, label='回帰直線')

ax.set_title('広告費 vs 売上(単回帰)', fontsize=14, fontweight='bold')
ax.set_xlabel('広告費(万円)', fontsize=12)
ax.set_ylabel('売上(万円)', fontsize=12)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

評価指標の詳細

# 各評価指標の計算
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print("=== 回帰モデルの評価 ===")
print(f"MSE  (平均二乗誤差):       {mse:.2f}")
print(f"RMSE (二乗平均平方根誤差): {rmse:.2f}")
print(f"MAE  (平均絶対誤差):       {mae:.2f}")
print(f"R²   (決定係数):           {r2:.4f}")

各指標の比較

指標 計算方法 値の範囲 特徴
MSE Σ(予測-実際)²/n 0以上 外れ値に敏感(二乗のため)
RMSE √MSE 0以上 目的変数と同じ単位で解釈しやすい
MAE Σ 予測-実際 /n
1 - SS_res/SS_tot -∞~1 1に近いほどモデルの説明力が高い

R²(決定係数)の解釈

# R²の解釈ガイド
r2_guide = pd.DataFrame({
    'R²の範囲': ['0.9 ~ 1.0', '0.7 ~ 0.9', '0.5 ~ 0.7', '0.3 ~ 0.5', '0.0 ~ 0.3'],
    '解釈': ['非常に良い適合', '良い適合', 'まずまずの適合', '弱い適合', 'モデルの説明力が低い'],
    '実務での判断': ['実運用可能', '改善の余地あり', '特徴量の追加を検討', 'モデル変更を検討', '根本的な見直しが必要']
})
print(r2_guide.to_string(index=False))

残差プロット

残差(予測値 - 実際の値)のパターンを確認し、モデルの妥当性を検証します。

residuals = y_test - y_pred

fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# 残差 vs 予測値
axes[0].scatter(y_pred, residuals, alpha=0.6, color='#2196F3')
axes[0].axhline(y=0, color='red', linestyle='--', linewidth=1.5)
axes[0].set_title('残差 vs 予測値', fontsize=13, fontweight='bold')
axes[0].set_xlabel('予測値')
axes[0].set_ylabel('残差')
axes[0].grid(True, alpha=0.3)

# 残差のヒストグラム
axes[1].hist(residuals, bins=10, color='#4CAF50', edgecolor='white', alpha=0.8)
axes[1].axvline(x=0, color='red', linestyle='--', linewidth=1.5)
axes[1].set_title('残差の分布', fontsize=13, fontweight='bold')
axes[1].set_xlabel('残差')
axes[1].set_ylabel('頻度')

# 予測 vs 実測
axes[2].scatter(y_test, y_pred, alpha=0.6, color='#FF9800')
min_val = min(y_test.min(), y_pred.min())
max_val = max(y_test.max(), y_pred.max())
axes[2].plot([min_val, max_val], [min_val, max_val], 'r--', linewidth=2, label='理想線 (y=x)')
axes[2].set_title('実測値 vs 予測値', fontsize=13, fontweight='bold')
axes[2].set_xlabel('実測値')
axes[2].set_ylabel('予測値')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

残差プロットの読み方

パターン 意味 対策
ランダムに散らばる 良いモデル そのまま採用
扇形に広がる 分散が不均一 対数変換、加重回帰
U字・逆U字型 非線形性がある 多項式特徴量、非線形モデル
偏りがある 系統的な誤差 特徴量の追加

重回帰分析

複数の特徴量から目的変数を予測します。

# 重回帰用データ:不動産価格予測
np.random.seed(42)
n = 200
area = np.random.uniform(20, 100, n)           # 面積(m²)
age = np.random.randint(0, 40, n)              # 築年数
distance = np.random.uniform(1, 30, n)         # 駅からの距離(分)
floor = np.random.randint(1, 20, n)            # 階数

# 家賃の生成(真のモデル + ノイズ)
rent = 30000 + 2500 * area - 1500 * age - 800 * distance + 500 * floor + np.random.normal(0, 10000, n)

property_df = pd.DataFrame({
    '面積': area,
    '築年数': age,
    '駅距離': distance,
    '階数': floor,
    '家賃': rent
})

print(property_df.describe().round(0))

# 相関の確認
print("\n家賃との相関:")
print(property_df.corr()['家賃'].sort_values(ascending=False))

重回帰モデルの構築

# 特徴量と目的変数
X = property_df[['面積', '築年数', '駅距離', '階数']]
y = property_df['家賃']

# 分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# モデルの学習
model = LinearRegression()
model.fit(X_train, y_train)

# 係数の確認
coef_df = pd.DataFrame({
    '特徴量': X.columns,
    '係数': model.coef_,
    '影響の方向': ['正(面積が大きいほど高い)', '負(古いほど安い)', '負(遠いほど安い)', '正(高階ほど高い)']
})
print("=== 重回帰の係数 ===")
print(coef_df)
print(f"\n切片: {model.intercept_:.0f}")

# 評価
y_pred = model.predict(X_test)
print(f"\nRMSE: {np.sqrt(mean_squared_error(y_test, y_pred)):.0f}円")
print(f"MAE:  {mean_absolute_error(y_test, y_pred):.0f}円")
print(f"R²:   {r2_score(y_test, y_pred):.4f}")

特徴量の重要度と選択

# 係数の絶対値で比較(標準化後)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_train)
model_scaled = LinearRegression()
model_scaled.fit(X_scaled, y_train)

importance = pd.DataFrame({
    '特徴量': X.columns,
    '標準化係数(絶対値)': np.abs(model_scaled.coef_)
}).sort_values('標準化係数(絶対値)', ascending=False)

print("=== 特徴量の重要度(標準化後) ===")
print(importance)

# 可視化
fig, ax = plt.subplots(figsize=(8, 4))
ax.barh(importance['特徴量'], importance['標準化係数(絶対値)'], color='#2196F3', edgecolor='white')
ax.set_title('特徴量の重要度(標準化係数)', fontsize=14, fontweight='bold')
ax.set_xlabel('標準化係数の絶対値')
ax.invert_yaxis()

plt.tight_layout()
plt.show()

新しいデータの予測

# 新しい物件の家賃を予測
new_property = pd.DataFrame({
    '面積': [65],
    '築年数': [5],
    '駅距離': [8],
    '階数': [10]
})

predicted_rent = model.predict(new_property)
print(f"予測された家賃: {predicted_rent[0]:,.0f}円")
print(f"\n物件情報:")
print(f"  面積: 65m², 築年数: 5年, 駅距離: 8分, 階数: 10階")

実践ワーク

# 実践:自動車燃費予測
from sklearn.datasets import fetch_openml

# Auto MPGデータセットの代用
np.random.seed(42)
n = 300
cylinders = np.random.choice([4, 6, 8], n, p=[0.5, 0.3, 0.2])
displacement = cylinders * 30 + np.random.normal(0, 20, n)
horsepower = displacement * 0.6 + np.random.normal(0, 15, n)
weight = displacement * 8 + np.random.normal(0, 200, n)
acceleration = 25 - horsepower * 0.05 + np.random.normal(0, 2, n)

mpg = 50 - 0.005 * weight - 0.03 * horsepower + 0.8 * acceleration + np.random.normal(0, 3, n)

auto_df = pd.DataFrame({
    'cylinders': cylinders,
    'displacement': displacement,
    'horsepower': horsepower,
    'weight': weight,
    'acceleration': acceleration,
    'mpg': mpg
})

# 課題1: 相関行列を確認し、mpgと最も相関の高い特徴量を見つける
print("=== mpgとの相関 ===")
print(auto_df.corr()['mpg'].sort_values(ascending=False))

# 課題2: 重回帰モデルを構築
X = auto_df[['displacement', 'horsepower', 'weight', 'acceleration']]
y = auto_df['mpg']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

# 課題3: 評価指標をすべて計算
print(f"\n=== 評価結果 ===")
print(f"RMSE: {np.sqrt(mean_squared_error(y_test, y_pred)):.2f}")
print(f"MAE:  {mean_absolute_error(y_test, y_pred):.2f}")
print(f"R²:   {r2_score(y_test, y_pred):.4f}")

# 課題4: 残差プロットを作成
residuals = y_test - y_pred
fig, ax = plt.subplots(figsize=(8, 5))
ax.scatter(y_pred, residuals, alpha=0.5, color='#2196F3')
ax.axhline(y=0, color='red', linestyle='--')
ax.set_title('残差プロット')
ax.set_xlabel('予測値')
ax.set_ylabel('残差')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

まとめと次回の準備

  • 単回帰は1つの特徴量、重回帰は複数の特徴量で目的変数を予測する
  • scikit-learn の LinearRegressionfit()predict() の流れでモデルを構築する
  • 評価指標は RMSE(元の単位で解釈)と (説明力、1に近いほど良い)が最重要
  • 残差プロットでモデルの妥当性を検証する。ランダムに散らばっていれば良いモデル
  • 特徴量の重要度は標準化後の係数の絶対値で比較する
  • 次回は分類を学ぶ。ロジスティック回帰と決定木でカテゴリ予測に進む

参考文献

Lecture 9分類 — カテゴリを予測する

12:00

分類 — カテゴリを予測する

この講義で学ぶこと

分類(Classification)は、入力データがどのカテゴリに属するかを予測するタスクです。本講義では、ロジスティック回帰と決定木の2つのアルゴリズムを実装し、混同行列・適合率・再現率・F1・ROC曲線といった分類特有の評価指標を理解します。

ロジスティック回帰

名前に「回帰」とあるが、実際は分類アルゴリズムです。線形回帰の出力にシグモイド関数を適用して、0から1の確率を出力します。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import (accuracy_score, precision_score, recall_score,
                             f1_score, confusion_matrix, classification_report,
                             roc_curve, roc_auc_score)
from sklearn.preprocessing import StandardScaler

plt.rcParams['font.family'] = 'MS Gothic'
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42)

サンプルデータ:顧客の離脱予測

# 顧客離脱データの作成
n = 500
tenure = np.random.randint(1, 72, n)           # 契約期間(月)
monthly_charge = np.random.uniform(20, 100, n)  # 月額料金
support_calls = np.random.poisson(2, n)         # サポート問い合わせ回数

# 離脱確率の計算(ロジスティック的な生成)
logit = -2 + 0.03 * monthly_charge - 0.05 * tenure + 0.4 * support_calls
prob = 1 / (1 + np.exp(-logit))
churn = (np.random.random(n) < prob).astype(int)

churn_df = pd.DataFrame({
    '契約期間': tenure,
    '月額料金': monthly_charge,
    'サポート問合せ': support_calls,
    '離脱': churn
})

print(f"データ形状: {churn_df.shape}")
print(f"離脱率: {churn_df['離脱'].mean():.1%}")
print(churn_df.head(10))

ロジスティック回帰の実装

# 特徴量と目的変数
X = churn_df[['契約期間', '月額料金', 'サポート問合せ']]
y = churn_df['離脱']

# データの分割
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

# 標準化(ロジスティック回帰では推奨)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# モデルの学習
log_model = LogisticRegression(random_state=42)
log_model.fit(X_train_scaled, y_train)

# 予測
y_pred = log_model.predict(X_test_scaled)
y_prob = log_model.predict_proba(X_test_scaled)[:, 1]  # 離脱確率

# 係数の確認
coef_df = pd.DataFrame({
    '特徴量': X.columns,
    '係数': log_model.coef_[0],
    '解釈': ['負:契約期間が長いほど離脱しにくい',
             '正:料金が高いほど離脱しやすい',
             '正:問合せが多いほど離脱しやすい']
})
print("=== ロジスティック回帰の係数 ===")
print(coef_df)

混同行列(Confusion Matrix)

# 混同行列
cm = confusion_matrix(y_test, y_pred)

fig, ax = plt.subplots(figsize=(7, 5))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=ax,
            xticklabels=['継続', '離脱'], yticklabels=['継続', '離脱'])
ax.set_title('混同行列', fontsize=14, fontweight='bold')
ax.set_xlabel('予測', fontsize=12)
ax.set_ylabel('実際', fontsize=12)

plt.tight_layout()
plt.show()

# 各セルの意味
print("=== 混同行列の読み方 ===")
tn, fp, fn, tp = cm.ravel()
print(f"True Negative  (TN): {tn}  → 継続を正しく「継続」と予測")
print(f"False Positive (FP): {fp}  → 継続を誤って「離脱」と予測(偽陽性)")
print(f"False Negative (FN): {fn}  → 離脱を誤って「継続」と予測(偽陰性)")
print(f"True Positive  (TP): {tp}  → 離脱を正しく「離脱」と予測")

分類の評価指標

Accuracy / Precision / Recall / F1

accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)

print("=== 分類評価指標 ===")
print(f"正解率(Accuracy):  {accuracy:.3f}  = (TP+TN) / 全体")
print(f"適合率(Precision): {precision:.3f}  = TP / (TP+FP)")
print(f"再現率(Recall):    {recall:.3f}  = TP / (TP+FN)")
print(f"F1スコア:            {f1:.3f}  = 2×P×R / (P+R)")

# 分類レポート
print("\n=== 分類レポート ===")
print(classification_report(y_test, y_pred, target_names=['継続', '離脱']))

各指標の使い分け

指標 重視する場面
Accuracy クラスが均等な場合 一般的な分類問題
Precision FP(偽陽性)を減らしたい スパム検出(正常メールを誤ってスパムにしたくない)
Recall FN(偽陰性)を減らしたい がん検出(がんを見逃したくない)
F1 PrecisionとRecallのバランス クラス不均衡がある場合

ROC曲線とAUC

# ROC曲線
fpr, tpr, thresholds = roc_curve(y_test, y_prob)
auc = roc_auc_score(y_test, y_prob)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# ROC曲線
axes[0].plot(fpr, tpr, 'b-', linewidth=2, label=f'ロジスティック回帰 (AUC = {auc:.3f})')
axes[0].plot([0, 1], [0, 1], 'r--', linewidth=1, label='ランダム (AUC = 0.500)')
axes[0].fill_between(fpr, tpr, alpha=0.1, color='blue')
axes[0].set_title('ROC曲線', fontsize=14, fontweight='bold')
axes[0].set_xlabel('偽陽性率 (FPR)', fontsize=12)
axes[0].set_ylabel('真陽性率 (TPR)', fontsize=12)
axes[0].legend(fontsize=11)
axes[0].grid(True, alpha=0.3)

# 予測確率の分布
axes[1].hist(y_prob[y_test == 0], bins=20, alpha=0.5, color='#2196F3', label='継続')
axes[1].hist(y_prob[y_test == 1], bins=20, alpha=0.5, color='#F44336', label='離脱')
axes[1].axvline(x=0.5, color='green', linestyle='--', label='閾値 = 0.5')
axes[1].set_title('予測確率の分布', fontsize=14, fontweight='bold')
axes[1].set_xlabel('離脱確率', fontsize=12)
axes[1].set_ylabel('頻度', fontsize=12)
axes[1].legend(fontsize=11)

plt.tight_layout()
plt.show()

print(f"AUCスコア: {auc:.3f}")
print("  AUC = 1.0: 完全な分類")
print("  AUC = 0.5: ランダムと同等(モデルに意味がない)")

決定木(Decision Tree)

決定木は、特徴量の条件分岐によってデータを分類する直感的なアルゴリズムです。

# 決定木モデル
tree_model = DecisionTreeClassifier(max_depth=3, random_state=42)
tree_model.fit(X_train, y_train)
y_pred_tree = tree_model.predict(X_test)

# 評価
print("=== 決定木の評価 ===")
print(classification_report(y_test, y_pred_tree, target_names=['継続', '離脱']))

# 決定木の可視化
fig, ax = plt.subplots(figsize=(18, 8))
plot_tree(tree_model, feature_names=X.columns, class_names=['継続', '離脱'],
          filled=True, rounded=True, fontsize=10, ax=ax)
ax.set_title('決定木の構造', fontsize=14, fontweight='bold')

plt.tight_layout()
plt.show()

# 特徴量の重要度
importance_df = pd.DataFrame({
    '特徴量': X.columns,
    '重要度': tree_model.feature_importances_
}).sort_values('重要度', ascending=False)

print("\n特徴量の重要度:")
print(importance_df)

ロジスティック回帰 vs 決定木

特徴 ロジスティック回帰 決定木
解釈性 係数で各特徴量の影響を定量化 ツリー構造で直感的に理解
非線形性 線形境界のみ 非線形境界を自然に学習
前処理 標準化が推奨 不要(スケール非依存)
過学習リスク 比較的低い 深い木では高い
確率出力 predict_proba で確率出力 葉ノードの比率で確率出力

モデルの比較

# 2つのモデルのROC曲線を比較
y_prob_tree = tree_model.predict_proba(X_test)[:, 1]

fig, ax = plt.subplots(figsize=(8, 6))

for name, y_p in [('ロジスティック回帰', y_prob), ('決定木', y_prob_tree)]:
    fpr, tpr, _ = roc_curve(y_test, y_p)
    auc_val = roc_auc_score(y_test, y_p)
    ax.plot(fpr, tpr, linewidth=2, label=f'{name} (AUC = {auc_val:.3f})')

ax.plot([0, 1], [0, 1], 'k--', linewidth=1)
ax.set_title('モデル比較:ROC曲線', fontsize=14, fontweight='bold')
ax.set_xlabel('偽陽性率 (FPR)')
ax.set_ylabel('真陽性率 (TPR)')
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

実践ワーク

from sklearn.datasets import load_breast_cancer

# 乳がんデータセット(569件、30特徴量、2クラス)
cancer = load_breast_cancer()
X = pd.DataFrame(cancer.data, columns=cancer.feature_names)
y = cancer.target  # 0: malignant(悪性), 1: benign(良性)

print(f"データ形状: {X.shape}")
print(f"クラス分布: {np.bincount(y)}")
print(f"クラス名: {cancer.target_names}")

# 課題1: データを分割(70:30、層化抽出)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

# 課題2: ロジスティック回帰モデルを構築(標準化あり)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

log_model = LogisticRegression(max_iter=10000, random_state=42)
log_model.fit(X_train_scaled, y_train)
y_pred_log = log_model.predict(X_test_scaled)

# 課題3: 決定木モデルを構築(max_depth=5)
tree_model = DecisionTreeClassifier(max_depth=5, random_state=42)
tree_model.fit(X_train, y_train)
y_pred_tree = tree_model.predict(X_test)

# 課題4: 両モデルの評価を比較
print("\n=== ロジスティック回帰 ===")
print(classification_report(y_test, y_pred_log, target_names=cancer.target_names))

print("=== 決定木 ===")
print(classification_report(y_test, y_pred_tree, target_names=cancer.target_names))

# 課題5: ROC曲線を描いて AUC を比較
y_prob_log = log_model.predict_proba(X_test_scaled)[:, 1]
y_prob_tree = tree_model.predict_proba(X_test)[:, 1]
print(f"ロジスティック回帰 AUC: {roc_auc_score(y_test, y_prob_log):.3f}")
print(f"決定木 AUC: {roc_auc_score(y_test, y_prob_tree):.3f}")

まとめと次回の準備

  • ロジスティック回帰は線形分類器で、シグモイド関数を通じて確率を出力する
  • 決定木は条件分岐でデータを分類し、可視化すると解釈しやすい
  • 混同行列(TP, TN, FP, FN)から Accuracy, Precision, Recall, F1 を計算する
  • Precision(偽陽性を減らしたい場合)と Recall(偽陰性を減らしたい場合)のトレードオフを理解する
  • ROC曲線とAUCでモデルの総合的な分類性能を比較できる(1.0が最高、0.5がランダム)
  • 次回は総合演習として、住宅価格予測プロジェクトで EDA からモデル評価までの全工程を通して実践する

参考文献

Lecture 10総合演習 — 住宅価格予測プロジェクト

15:00

総合演習 — 住宅価格予測プロジェクト

この講義で学ぶこと

本講義では、これまで学んだ全てのスキルを統合し、California Housing データセットを使った住宅価格予測プロジェクトをEDAからモデル評価・解釈まで一気通貫で実践します。データサイエンスプロジェクトの全工程を体験することで、実務に通じるワークフローを身につけます。

プロジェクトの全体像(CRISP-DM に沿って)

Phase 1: ビジネス理解
  └ 「カリフォルニアの地区ごとの住宅価格を予測する」

Phase 2: データ理解(EDA)
  └ データの読み込み・全体像把握・分布確認・相関分析

Phase 3: データ準備(前処理・特徴量エンジニアリング)
  └ 欠損値処理・外れ値処理・新特徴量の作成・標準化

Phase 4: モデリング
  └ 複数モデルの構築と比較

Phase 5: 評価
  └ テストデータでの性能検証・残差分析

Phase 6: 解釈
  └ 特徴量の重要度分析・ビジネスへの示唆

Phase 1: データの読み込みとビジネス理解

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
import warnings
warnings.filterwarnings('ignore')

plt.rcParams['font.family'] = 'MS Gothic'
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42)

# データの読み込み
housing = fetch_california_housing()
df = pd.DataFrame(housing.data, columns=housing.feature_names)
df['MedHouseVal'] = housing.target  # 目的変数:住宅価格の中央値(10万ドル単位)

print("=== California Housing Dataset ===")
print(f"データ形状: {df.shape}")
print(f"\n特徴量の説明:")
descriptions = {
    'MedInc': '地区の所得中央値(万ドル)',
    'HouseAge': '住宅の築年数中央値',
    'AveRooms': '世帯あたりの平均部屋数',
    'AveBedrms': '世帯あたりの平均寝室数',
    'Population': '地区の人口',
    'AveOccup': '世帯あたりの平均居住者数',
    'Latitude': '緯度',
    'Longitude': '経度',
    'MedHouseVal': '住宅価格中央値(10万ドル単位)← 目的変数'
}
for col, desc in descriptions.items():
    print(f"  {col}: {desc}")

Phase 2: 探索的データ分析(EDA)

2-1: 基本統計量の確認

print("\n=== 基本統計量 ===")
print(df.describe().round(2))

print("\n=== 欠損値 ===")
print(df.isnull().sum())

print("\n=== データ型 ===")
print(df.dtypes)

2-2: 目的変数の分布

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# ヒストグラム
axes[0].hist(df['MedHouseVal'], bins=50, color='#2196F3', edgecolor='white', alpha=0.8)
axes[0].axvline(df['MedHouseVal'].mean(), color='red', linestyle='--',
                label=f"平均: {df['MedHouseVal'].mean():.2f}")
axes[0].axvline(df['MedHouseVal'].median(), color='green', linestyle='-.',
                label=f"中央値: {df['MedHouseVal'].median():.2f}")
axes[0].set_title('住宅価格の分布', fontsize=14, fontweight='bold')
axes[0].set_xlabel('住宅価格(10万ドル)')
axes[0].set_ylabel('頻度')
axes[0].legend()

# 箱ひげ図
axes[1].boxplot(df['MedHouseVal'], vert=True)
axes[1].set_title('住宅価格の箱ひげ図', fontsize=14, fontweight='bold')
axes[1].set_ylabel('住宅価格(10万ドル)')

plt.tight_layout()
plt.show()

# 5.001でキャップされているデータの確認
cap_count = (df['MedHouseVal'] >= 5.0).sum()
print(f"価格が5.0以上(キャップ値): {cap_count}件 ({cap_count/len(df)*100:.1f}%)")

2-3: 特徴量の分布

fig, axes = plt.subplots(2, 4, figsize=(20, 10))
axes = axes.flatten()

for i, col in enumerate(housing.feature_names):
    axes[i].hist(df[col], bins=40, color='#4CAF50', edgecolor='white', alpha=0.8)
    axes[i].set_title(col, fontsize=12, fontweight='bold')
    axes[i].axvline(df[col].mean(), color='red', linestyle='--', alpha=0.7)

plt.suptitle('特徴量の分布', fontsize=16, fontweight='bold')
plt.tight_layout()
plt.show()

2-4: 相関分析

# 相関行列
fig, ax = plt.subplots(figsize=(10, 8))

corr = df.corr()
mask = np.triu(np.ones_like(corr, dtype=bool))
sns.heatmap(corr, annot=True, cmap='RdBu_r', center=0, fmt='.2f',
            mask=mask, square=True, linewidths=0.5, ax=ax)
ax.set_title('相関行列', fontsize=14, fontweight='bold')

plt.tight_layout()
plt.show()

# 目的変数との相関を降順で表示
print("=== 住宅価格との相関 ===")
print(corr['MedHouseVal'].sort_values(ascending=False))

2-5: 散布図による関係性の確認

# 住宅価格と相関の高い特徴量の散布図
top_features = ['MedInc', 'AveRooms', 'HouseAge', 'Latitude']

fig, axes = plt.subplots(2, 2, figsize=(14, 10))
axes = axes.flatten()

for i, col in enumerate(top_features):
    axes[i].scatter(df[col], df['MedHouseVal'], alpha=0.1, s=5, color='#2196F3')
    axes[i].set_title(f'{col} vs MedHouseVal (r={corr.loc[col, "MedHouseVal"]:.3f})',
                      fontsize=12, fontweight='bold')
    axes[i].set_xlabel(col)
    axes[i].set_ylabel('住宅価格')

plt.suptitle('住宅価格との関係(上位4変数)', fontsize=15, fontweight='bold')
plt.tight_layout()
plt.show()

2-6: 地理的可視化

fig, ax = plt.subplots(figsize=(10, 8))

scatter = ax.scatter(df['Longitude'], df['Latitude'],
                     c=df['MedHouseVal'], cmap='YlOrRd',
                     alpha=0.3, s=5)
ax.set_title('カリフォルニア住宅価格の地理分布', fontsize=14, fontweight='bold')
ax.set_xlabel('経度')
ax.set_ylabel('緯度')
plt.colorbar(scatter, label='住宅価格(10万ドル)')

plt.tight_layout()
plt.show()

Phase 3: データ準備

3-1: 外れ値の処理

# キャップ値(5.001)の除去
df_clean = df[df['MedHouseVal'] < 5.0].copy()
print(f"キャップ値除去後: {len(df_clean)}件 (除去: {len(df) - len(df_clean)}件)")

# AveRooms, AveBedrms, AveOccup の外れ値をIQR法でクリップ
for col in ['AveRooms', 'AveBedrms', 'AveOccup', 'Population']:
    Q1 = df_clean[col].quantile(0.01)
    Q3 = df_clean[col].quantile(0.99)
    df_clean[col] = df_clean[col].clip(Q1, Q3)

print(f"外れ値処理後のデータ形状: {df_clean.shape}")

3-2: 特徴量エンジニアリング

既存の特徴量から新しい特徴量を作成して、モデルの予測力を高めます。

# 新しい特徴量の作成
df_clean['RoomsPerBedroom'] = df_clean['AveRooms'] / (df_clean['AveBedrms'] + 0.001)
df_clean['PopulationPerHousehold'] = df_clean['Population'] / (df_clean['AveOccup'] + 0.001)
df_clean['IncomePerRoom'] = df_clean['MedInc'] / (df_clean['AveRooms'] + 0.001)

# 追加した特徴量の相関確認
new_features = ['RoomsPerBedroom', 'PopulationPerHousehold', 'IncomePerRoom']
print("=== 新特徴量と住宅価格の相関 ===")
for feat in new_features:
    r = df_clean[feat].corr(df_clean['MedHouseVal'])
    print(f"  {feat}: {r:.3f}")

3-3: データの分割と標準化

# 特徴量と目的変数
feature_cols = housing.feature_names + new_features
X = df_clean[feature_cols]
y = df_clean['MedHouseVal']

# 分割
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

print(f"訓練データ: {X_train.shape}")
print(f"テストデータ: {X_test.shape}")

# 標準化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

Phase 4: モデリング

4-1: 複数モデルの構築と比較

# モデルの定義
models = {
    '線形回帰': LinearRegression(),
    'Ridge回帰': Ridge(alpha=1.0),
    'Lasso回帰': Lasso(alpha=0.01),
    '決定木': DecisionTreeRegressor(max_depth=8, random_state=42),
    'ランダムフォレスト': RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42),
    '勾配ブースティング': GradientBoostingRegressor(n_estimators=100, max_depth=5, random_state=42)
}

# 各モデルの学習と評価
results = []

for name, model in models.items():
    # 線形モデルは標準化データ、木系はそのまま
    if name in ['線形回帰', 'Ridge回帰', 'Lasso回帰']:
        model.fit(X_train_scaled, y_train)
        y_pred = model.predict(X_test_scaled)
        cv_scores = cross_val_score(model, X_train_scaled, y_train, cv=5,
                                    scoring='neg_mean_squared_error')
    else:
        model.fit(X_train, y_train)
        y_pred = model.predict(X_test)
        cv_scores = cross_val_score(model, X_train, y_train, cv=5,
                                    scoring='neg_mean_squared_error')

    rmse = np.sqrt(mean_squared_error(y_test, y_pred))
    mae = mean_absolute_error(y_test, y_pred)
    r2 = r2_score(y_test, y_pred)
    cv_rmse = np.sqrt(-cv_scores.mean())

    results.append({
        'モデル': name,
        'RMSE': rmse,
        'MAE': mae,
        'R²': r2,
        'CV-RMSE': cv_rmse
    })

results_df = pd.DataFrame(results).sort_values('RMSE')
print("=== モデル比較結果 ===")
print(results_df.to_string(index=False))

4-2: 結果の可視化

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# RMSE比較
colors = ['#F44336' if m == results_df.iloc[0]['モデル'] else '#2196F3'
          for m in results_df['モデル']]
axes[0].barh(results_df['モデル'], results_df['RMSE'], color=colors, edgecolor='white')
axes[0].set_title('モデル別 RMSE(低いほど良い)', fontsize=13, fontweight='bold')
axes[0].set_xlabel('RMSE')
axes[0].invert_yaxis()

# R²比較
colors_r2 = ['#4CAF50' if m == results_df.iloc[0]['モデル'] else '#FF9800'
             for m in results_df['モデル']]
axes[1].barh(results_df['モデル'], results_df['R²'], color=colors_r2, edgecolor='white')
axes[1].set_title('モデル別 R²(高いほど良い)', fontsize=13, fontweight='bold')
axes[1].set_xlabel('R²')
axes[1].invert_yaxis()

plt.tight_layout()
plt.show()

Phase 5: 最良モデルの詳細評価

# 最良モデル(勾配ブースティングを想定)の詳細分析
best_model = GradientBoostingRegressor(n_estimators=100, max_depth=5, random_state=42)
best_model.fit(X_train, y_train)
y_pred_best = best_model.predict(X_test)

# 残差分析
residuals = y_test - y_pred_best

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 予測 vs 実測
axes[0, 0].scatter(y_test, y_pred_best, alpha=0.2, s=10, color='#2196F3')
min_val = min(y_test.min(), y_pred_best.min())
max_val = max(y_test.max(), y_pred_best.max())
axes[0, 0].plot([min_val, max_val], [min_val, max_val], 'r--', linewidth=2)
axes[0, 0].set_title('実測値 vs 予測値', fontsize=13, fontweight='bold')
axes[0, 0].set_xlabel('実測値')
axes[0, 0].set_ylabel('予測値')

# 残差 vs 予測値
axes[0, 1].scatter(y_pred_best, residuals, alpha=0.2, s=10, color='#4CAF50')
axes[0, 1].axhline(y=0, color='red', linestyle='--', linewidth=1.5)
axes[0, 1].set_title('残差 vs 予測値', fontsize=13, fontweight='bold')
axes[0, 1].set_xlabel('予測値')
axes[0, 1].set_ylabel('残差')

# 残差の分布
axes[1, 0].hist(residuals, bins=50, color='#FF9800', edgecolor='white', alpha=0.8)
axes[1, 0].axvline(x=0, color='red', linestyle='--', linewidth=1.5)
axes[1, 0].set_title('残差の分布', fontsize=13, fontweight='bold')
axes[1, 0].set_xlabel('残差')
axes[1, 0].set_ylabel('頻度')

# 予測値の分布
axes[1, 1].hist(y_test, bins=40, alpha=0.5, color='#2196F3', label='実測値')
axes[1, 1].hist(y_pred_best, bins=40, alpha=0.5, color='#F44336', label='予測値')
axes[1, 1].set_title('実測値 vs 予測値の分布', fontsize=13, fontweight='bold')
axes[1, 1].set_xlabel('住宅価格')
axes[1, 1].set_ylabel('頻度')
axes[1, 1].legend()

plt.suptitle('最良モデルの残差分析', fontsize=15, fontweight='bold')
plt.tight_layout()
plt.show()

# 残差の統計量
print("=== 残差の統計量 ===")
print(f"残差の平均: {residuals.mean():.4f}(0に近いほど良い)")
print(f"残差の標準偏差: {residuals.std():.4f}")
print(f"残差の歪度: {residuals.skew():.4f}")

Phase 6: 特徴量の重要度と解釈

# 特徴量の重要度(勾配ブースティング)
importance = pd.DataFrame({
    '特徴量': feature_cols,
    '重要度': best_model.feature_importances_
}).sort_values('重要度', ascending=False)

fig, ax = plt.subplots(figsize=(10, 6))
ax.barh(importance['特徴量'], importance['重要度'], color='#2196F3', edgecolor='white')
ax.set_title('特徴量の重要度(勾配ブースティング)', fontsize=14, fontweight='bold')
ax.set_xlabel('重要度')
ax.invert_yaxis()

plt.tight_layout()
plt.show()

print("=== 特徴量重要度ランキング ===")
for i, row in importance.iterrows():
    print(f"  {row['特徴量']:25s}: {row['重要度']:.4f}")

ビジネスへの示唆

# 予測値の上位・下位の特徴を比較
test_result = X_test.copy()
test_result['実測値'] = y_test
test_result['予測値'] = y_pred_best

top_10pct = test_result.nlargest(int(len(test_result)*0.1), '予測値')
bottom_10pct = test_result.nsmallest(int(len(test_result)*0.1), '予測値')

comparison = pd.DataFrame({
    '高価格帯(上位10%)': top_10pct[feature_cols].mean(),
    '低価格帯(下位10%)': bottom_10pct[feature_cols].mean()
}).round(2)

print("\n=== 高価格帯 vs 低価格帯の特徴比較 ===")
print(comparison)

プロジェクトまとめレポート

# 最終レポート
best_rmse = np.sqrt(mean_squared_error(y_test, y_pred_best))
best_mae = mean_absolute_error(y_test, y_pred_best)
best_r2 = r2_score(y_test, y_pred_best)

print("=" * 60)
print("   住宅価格予測プロジェクト 最終レポート")
print("=" * 60)
print(f"\nデータ:")
print(f"  総データ数: {len(df_clean):,}件")
print(f"  訓練データ: {len(X_train):,}件")
print(f"  テストデータ: {len(X_test):,}件")
print(f"  特徴量数: {len(feature_cols)}個(元8 + 新規3)")

print(f"\n最良モデル: 勾配ブースティング")
print(f"  RMSE: {best_rmse:.4f}(10万ドル)= 約${best_rmse*100000:,.0f}")
print(f"  MAE:  {best_mae:.4f}(10万ドル)= 約${best_mae*100000:,.0f}")
print(f"  R²:   {best_r2:.4f}(説明力: {best_r2*100:.1f}%)")

print(f"\n上位3重要特徴量:")
for i, row in importance.head(3).iterrows():
    print(f"  {i+1}. {row['特徴量']} (重要度: {row['重要度']:.4f})")

print(f"\n改善の方向性:")
print(f"  1. ハイパーパラメータチューニング(GridSearchCV)")
print(f"  2. アンサンブル手法の検討(Stacking)")
print(f"  3. 地理情報を活かした特徴量(近隣の平均価格など)")
print("=" * 60)

実践ワーク

このプロジェクトをさらに発展させてください。

# 発展課題1: Ridge/Lasso のハイパーパラメータを変えて比較
from sklearn.model_selection import GridSearchCV

param_grid = {'alpha': [0.001, 0.01, 0.1, 1.0, 10.0, 100.0]}
ridge_cv = GridSearchCV(Ridge(), param_grid, cv=5, scoring='neg_mean_squared_error')
ridge_cv.fit(X_train_scaled, y_train)
print(f"Ridge最適alpha: {ridge_cv.best_params_}")
print(f"Ridge最適RMSE: {np.sqrt(-ridge_cv.best_score_):.4f}")

# 発展課題2: ランダムフォレストの木の数を変えて学習曲線を描く
n_estimators_list = [10, 50, 100, 200, 500]
train_scores = []
test_scores = []

for n_est in n_estimators_list:
    rf = RandomForestRegressor(n_estimators=n_est, max_depth=10, random_state=42)
    rf.fit(X_train, y_train)
    train_scores.append(r2_score(y_train, rf.predict(X_train)))
    test_scores.append(r2_score(y_test, rf.predict(X_test)))

fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(n_estimators_list, train_scores, 'o-', label='訓練スコア', color='#2196F3')
ax.plot(n_estimators_list, test_scores, 's-', label='テストスコア', color='#F44336')
ax.set_title('ランダムフォレストの学習曲線', fontsize=14, fontweight='bold')
ax.set_xlabel('木の数(n_estimators)')
ax.set_ylabel('R²スコア')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

まとめと次回の準備

  • CRISP-DM のフレームワークに沿って、データ理解 → 前処理 → モデリング → 評価 → 解釈の全工程を実践した
  • EDA では分布・相関・地理的可視化を通じてデータの特性を深く理解した
  • 特徴量エンジニアリング(RoomsPerBedroom, IncomePerRoom など)でモデルの予測力を向上させた
  • 6つのモデル(線形回帰、Ridge、Lasso、決定木、ランダムフォレスト、勾配ブースティング)を比較し、最良モデルを選定した
  • 残差分析でモデルの妥当性を検証し、特徴量重要度でビジネスへの示唆を導き出した
  • 所得中央値(MedInc) が住宅価格の最大の決定要因であることが確認できた
  • このプロジェクトのワークフローは、実務のデータサイエンスプロジェクトにそのまま応用できる

参考文献