神田大樹の個人サイトです。

大きなデータの分布形状

  • Tag: BigQuery, Matplotlib
  • 大きなサンプルサイズのデータセットについて 分布のグラフを作成する方法。

    BigQueryオープンデータセットの ニューヨーク市シティバイクの利用データに対して 性別毎の利用者年齢の分布を調べます。

    環境

    from IPython.display import display, Markdown
    import sys
    
    print(f"{sys.version_info=}")
    # > sys.version_info=sys.version_info(major=3, minor=11, micro=7, releaselevel='final', serial=0)
    import matplotlib as mpl
    from matplotlib import pyplot as plt
    from matplotlib import ticker
    import numpy as np
    import pandas as pd
    
    pd.options.mode.copy_on_write = True
    
    print(f"{mpl.__version__=}")
    # > mpl.__version__='3.8.2'
    print(f"{np.__version__=}")
    # > np.__version__='1.26.2'
    print(f"{pd.__version__=}")
    # > pd.__version__='2.1.4'

    クエリの実行

    はじめに利用回数を性別毎、各年齢単位で集計します。

    WITH
    raw_data AS (
      SELECT
        starttime,
        birth_year,
        gender
      FROM `bigquery-public-data.new_york_citibike.citibike_trips`
    ),
    extract_year_from_starttime AS (
      SELECT
        starttime,
        birth_year,
        -- 利用した西暦を抽出
        EXTRACT(YEAR FROM starttime) AS year,
        gender
      FROM raw_data
    ),
    calculation_rider_age AS (
      SELECT
        starttime,
        birth_year,
        year,
        -- 利用者の生まれた西暦から利用時の年齢を計算
        year - birth_year AS rider_age,
        gender
      FROM extract_year_from_starttime
    ),
    count_rider_age AS (
      SELECT
        rider_age,
        gender,
        COUNT(1) AS num_rider
      FROM calculation_rider_age
      GROUP BY rider_age, gender
    )
    
    SELECT
    *
    FROM count_rider_age;

    グラフ作成

    CSVファイルにクエリ結果を出力した後、Pandasを用いて集計データを読み込みます。

    # NULLは除外
    df = (
        pd.read_csv("data/num_rider.csv")
        .dropna()
        )
    df.head()
    rider_age gender num_rider
    0 46 male 512864
    1 39 male 583923
    2 38 male 597902
    3 45 male 534975
    4 62 male 147757

    ドキュメントに従って、階段型グラフstairs()をつかって “histgram-like” なカウントデータのグラフ作成します。 (weightsにカウントデータを渡すことでhist()でもグラフの作成が可能ですが、混乱を避けるためにstairs()を使います。)

    # グラフ作成の補助関数を実装します
    def count_data_distribution(ax, x, y, bin_width):
        """ 階段型グラフのwrapper
        """
        heights = y
        positions = x
        # `stairs()` ではx軸のデータはy軸よりひとつ多い必要があります
        if len(positions) != (len(heights) + 1):
            positions = np.append(positions, [positions[-1] + bin_width])
    
        stairs = ax.stairs(heights, positions, fill=True)
        return ax, stairs
    
    def format_yticks(ax, format_func):
        """ y軸目盛のformatter
        """
        formatter = ticker.FuncFormatter(format_func)
        ax.yaxis.set_major_formatter(formatter)
        return ax
    
    bin_width = 1.0
    genders = ['female', 'male']
    
    fig, axes = plt.subplots(figsize=(9, 4), ncols=2, sharey=True, layout="tight")
    
    # 性別毎にグラフを作成します
    for ax, gender in zip(axes, genders):
        temp = (
              df
              .query("gender==@gender")
              .sort_values(["rider_age"], ignore_index=True)
              )
        x = temp["rider_age"].dropna().to_numpy()
        y = temp["num_rider"].dropna().to_numpy()
    
        count_data_distribution(ax, x, y, bin_width)
        ax.xaxis.set_major_locator(ticker.MultipleLocator(10))
        ax.set_xlabel("Age")
        ax.set_title(gender)
        ax.grid()
    
    format_yticks(axes[0], (lambda x, _: f"{int(x):,d}"))
    axes[0].set_ylabel("Number of Trips")
    
    fig.suptitle("Age Distribution of Citibike Trips by Gender")
    fig.savefig("images/distribution.jpeg");

    Age Distribution of Citibike Trips by Gender

    また累積分布のグラフも描画することで、 データ分布の様子を詳細に理解できます。

    # 性別毎に利用回数の総合計を計算します
    total_num_rider = (
        df
        .groupby(["gender"], as_index=False)
        ["num_rider"].sum()
        .rename(columns={"num_rider": "total_num_rider"})
    )
    
    # 累積割合を計算します
    df = (
        pd.merge(left=df, right=total_num_rider, on="gender", how="left")
        .sort_values(["gender", "rider_age"], ignore_index=True)
        .assign(
            percentage=lambda x: 100 * x["num_rider"] / x["total_num_rider"],
            cumlative=lambda x: x.groupby("gender")["percentage"].cumsum()
        )
    )
    
    fig, axes = plt.subplots(figsize=(9, 4), ncols=2, sharey=True, layout="tight")
    
    # 性別毎にグラフを作成します
    for ax, gender in zip(axes, genders):
        temp = (
              df
              .query("gender==@gender")
              .sort_values(["rider_age"], ignore_index=True)
              )
        x = temp["rider_age"].dropna().to_numpy()
        y = temp["cumlative"].dropna().to_numpy()
    
        count_data_distribution(ax, x, y, bin_width)
    
        ax.xaxis.set_major_locator(ticker.MultipleLocator(10))
    
        ax.set_xlabel("Age")
        ax.set_title(gender)
        ax.grid()
    
    axes[0].set_ylabel("Number of Trips")
    
    axes[0].yaxis.set_major_locator(ticker.MultipleLocator(25))
    format_yticks(axes[0], (lambda x, _: f"{int(x):d}%"))
    
    fig.suptitle("Cumulative Age Distribution of Citibike Trips by Gender")
    fig.savefig("images/cumulative_distribution.jpeg");

    Age Cumulative Distribution of Citibike Trips by Gender