pandasライブラリで複数のCSVデータをロードしマージする


【最終更新日】2023-06-23

pandasライブラリで複数のCSVデータをマージし、指定された形式でMatplotlib描画用のDataFrameに加工する方法について解説します。

今回取り上げる題材は自作の健康管理アプリで入力したデータの可視化画像の作成です。 健康管理アプリで入力したデータは可視化用途向けのRaspberryPiサーバーのデータベースに登録したもので、それをCSV出力したファイルを使用します。

使用スクリプトファイル名
PlotSleepManBar2Plot_3_pandas_month.py
データ種別ごとに使用するCSVファイル一覧 【datas/csv/】
データ種別睡眠管理CSV頻尿要因CSV
通常データ (2023-01〜2023-04)sleep_management.csvnocturia_factors.csv
欠損データ (2023-03)sleep_management_202303.csvnocturia_factors_202303.csv
【通常データと欠損データの場合の出力画像】
【参考URL】
【参考にした書籍】
第1章 DataFrameの基礎
第3章 pandasのデータ構造 【2.6 データのエクスポートとインポート】
第4章 データを組み立てる 【4.4 複数のデータセットをマージする】
第9章 applayによる関数の適用
第11章 日付/時刻データの操作 【11.9 日付によるデータの絞り込み】
Chapter1 Numpyの基礎
Chapter2 Numpy配列を操作する関数を知る: 2.5 最大値、最小値を抜き出す
【当サイトの関連リポジトリ】Personal Healthcare applications
健康管理(Android)アプリ登録画面とWebアプリのIF設計の詳細ついては下記をご覧ください
https://pipito-yukio.github.io/personal_healthcare/01_design_interface.html
【AndroidアプリとWebアプリのIF設計の概要】
1. マージする睡眠管理CSVデータと頻尿要因CSVデータ
(1) CSVデータ
(2) CSVデータに対応するテーブル定義 ※ sql/11_createtable.sql の抜粋
(3) DataFrame.join (※pidは無視) とほぼ同等のSQL
SELECT
  to_char(sm.measurement_day,'YYYY-MM-DD') as measurement_day
  ,to_char(wakeup_time,'HH24:MI:SS') as wakeup_time
  ,sleep_score
  ,to_char(sleeping_time, 'HH24:MI:SS') as sleeping_time
  ,to_char(deep_sleeping_time, 'HH24:MI') as deep_sleeping_time
  ,midnight_toilet_visits
FROM
  bodyhealth.person p
  INNER JOIN bodyhealth.sleep_management sm ON p.id = sm.pid
  INNER JOIN bodyhealth.nocturia_factors nf ON p.id = nf.pid
WHERE
  email=:emailAddress
  AND
  sm.measurement_day BETWEEN :startDay AND :endDay
  AND
  sm.measurement_day = nf.measurement_day
  ORDER BY sm.measurement_day
2. ソースコードの説明
2-1. pandasの処理コード
(1) 関数定義
  • 時刻文字列("%H:%M:%S")から秒部分を切り捨て
    • 【DataFrameの列データの加工】Series.apply(trimSecondsWithTime)
    • 【使用箇所】睡眠管理CSVの時刻列("時:分:秒")の変換
    • 【計算例】[入力] "05:30:00" => [出力] "05:30"
    def trimSecondsWithTime(strTime: str) -> str:
        """
        時刻文字列("%H:%M:%S")から秒部分をトリムする
        :param strTime: 時刻文字列("%H:%M:%S") ※欠損値(None)有り
        :return: 秒をトリムした時刻文字列
        """
        if strTime is None or pd.isna(strTime):  # pandasでは nan のチェックが必要
            return ""
    
        timeValues: List[str] = strTime.split(":")
        return f"{timeValues[0]}:{timeValues[1]}"
  • 時刻文字列("時:分")を分に変換
    • 【DataFrameの列データの加工】Series.apply(toMinute)
    • 【使用箇所】棒グラフ(単位は分)用データ変換
    • 【計算例】[入力] "05:30" => [出力] 330 (分)
    def toMinute(strTime: str) -> Optional[int]:
        """
        時刻文字列("時:分")を分に変換する
        :param strTime: 時刻文字列("時:分") ※欠損値有り(None)
        :return: 分(整数), NoneならNone
        """
        if strTime is None or pd.isna(strTime):  # pandasでは nan のチェックが必要
            return None
    
        times: List[str] = strTime.split(":")
        return int(times[0]) * 60 + int(times[1])
  • 分を時刻文字列("%H:%M")に変換
    • 【使用箇所】睡眠データプロット領域の左側Y軸ラベルの生成
    • 【計算例】[入力] 330 (分) => [出力] "05:30"
    def minuteToFormatTime(val_minutes: int) -> str:
        """
        分を時刻文字列("%H:%M")に変換する
        :param val_minutes: 分
        :return: 時刻文字列("%H:%M")
        """
        return f"{val_minutes // 60:#02d}:{val_minutes % 60:#02d}"
  • 年月(文字列)の末日を計算
    • 【使用箇所】入力パラメータ --year-month の末日計算時
    • 【計算例】[入力] "2023-03" => [出力] 31
    def calcEndOfMonth(str_year_month: str) -> int:
        """
        年月(文字列)の末日を計算する
        :param str_year_month: 年月(文字列, "-"区切り)
        :return: 末日
        """
        yearMonths = str_year_month.split("-")
        valYear, valMonth = int(yearMonths[0]), int(yearMonths[1])
        if valMonth == 12:
            valYear += 1
            valMonth = 1
        else:
            valMonth += 1
        # 月末日の翌月の1日
        valNextYearMonth = date(valYear, valMonth, 1)
        # 月末日の計算: 次の月-1日
        valLastDayOfMonth = valNextYearMonth - timedelta(days=1)
        return valLastDayOfMonth.day
  • 前日の就寝時刻を計算
    • 【使用箇所】夜間トイレ回数プロット領域のX軸ラベルリスト生成時
    • 【計算例】[入力] "2023-03-04", "06:15", "07:00" => [出力] "23:15"
    def calcBedTime(strDate: datetime, strWakeupTime: str, strSleepingTime: str
                    ) -> Optional[datetime]:
        """
        就寝時刻(前日)を計算する
        :param strDate: 測定日付文字列(ISO8601) ※必須
        :param strWakeupTime: 起床時刻文字列 ("%H:%M") ※必須
        :param strSleepingTime: 睡眠時間 ("%H:%M") ※任意 欠損値 None
        :return: (測定日付+起床時刻) - 睡眠時間
        """
        if strSleepingTime is None:
            return None
    
        wakeupDayTime: datetime = datetime.strptime(f"{strDate} {strWakeupTime}",
                                                    FMT_DATETIME)
        valMinutes: int = toMinute(strSleepingTime)
        return wakeupDayTime - timedelta(minutes=valMinutes)
  • X軸の日付ラベル文字列を生成
    • 【使用箇所】睡眠管理データプロット領域のX軸ラベルリスト生成時
    • 【計算例】[入力] 2023-03-01 => [出力] 2023-03-01 (水)
    def makeDateLabel(strIsoDay: str) -> str:
        """
        X軸の日付ラベル文字列を生成する\n
        [形式] "日 (曜日)"
        :param strIsoDay: ISO8601 日付文字列
        :return: 日付ラベル文字列
        """
        val_date: datetime = datetime.strptime(strIsoDay, FMT_DATE)
        weekday_name = JP_WEEK_DAY_NAMES[val_date.weekday()]
        return f"{val_date.day} ({weekday_name})"
(2) スクリプトメイン
  • 1-1. インポート ※pandas関連処理のみ
    import argparse
    import os
    from datetime import date, datetime, timedelta
    import pandas as pd
    from pandas.core.frame import DataFrame, Series
    # ...一部省略...
    import util.date_util as du
    
    
  • 1-2. 定数定義 ※pandas関連処理のみ
    # ISO8601フォーマット
    FMT_DATE: str = '%Y-%m-%d'
    # datetime変換フォーマット ※psqlでのCSVエクスポートが秒まで出力
    FMT_DATETIME: str = '%Y-%m-%d %H:%M:%S'
    # ...一部省略...
    # 睡眠管理データ(CSVファイル)で利用するカラム ※"pid"を除く
    SLEEP_MAN_COLS: List[str] = [
        "measurement_day", "wakeup_time", "sleep_score", "sleeping_time", "deep_sleeping_time"
    ]
    
    # 頻尿要因データ(CSVファイル)で利用するカラム ※"measurement_day"と"midnight_toilet_visits"
    NOCT_FACT_COLS: List[str] = ["measurement_day", "midnight_toilet_visits"]
  • 2. コマンドラインオプション ※すべて必須
    • プロット対象年月【 月間データ】: (例) -year-month 2023-03
    • 睡眠管理CSVデータファイルパス: (例) --sleep-man datas/csv/sleep_management_202303.csv
    • 頻尿要因CSVデータファイルパス: (例) --noct-fact datas/csv/nocturia_factors_202303.csv
    if __name__ == '__main__':
        parser: argparse.ArgumentParser = argparse.ArgumentParser()
        # プロット対象年月
        parser.add_argument("--year-month", type=str, required=True,
                            help="年月 (例) 2023-04")
        # 睡眠管理CSVデータファイルパス: 年月のみか、複数月
        parser.add_argument("--sleep-man", type=str, required=True,
                            help="datas/csv/sleep_management.csv")
        # 頻尿要因CSVデータファイルパス: 年月のみか、複数月 ※件数は一致すること
        parser.add_argument("--noct-fact", type=str, required=True,
                            help="datas/csv/nocturia_factors.csv")
        args: argparse.Namespace = parser.parse_args()
  • 3. CSVファイルの存在チェック
        path_sleepMan: str = os.path.expanduser(args.sleep_man)
        path_noctFact: str = os.path.expanduser(args.noct_fact)
        # スクリプト直下の相対ファイルか、ユーザホームのCSV格納ディレクトリのファイル
        if not os.path.exists(path_sleepMan) or not os.path.exists(path_noctFact):
            app_logger.warning("CSV not found.")
            exit(1)
  • 4. プロット処理年月の開始日と終了日(月末日)の計算
        year_month: str = args.year_month
        # 指定年月の開始日
        start_date: str = f"{year_month}-01"
        # 日付文字列チェック
        if not du.check_str_date(start_date):
            app_logger.warning("Invalid day format!")
            exit(1)
    
        # 指定年月の月末日
        endDay: int = calcEndOfMonth(year_month)
        # 指定年月の終了日
        end_date: str = f"{year_month}-{endDay:#02d}"
  • 5-1. 各CSVファイルごとに対応するDataFrameを生成する
    • ヘッダーは1行目
    • "measurement_day"列 を日付として扱う: FMT_DATE='%Y-%m-%d' ※ISO8601フォーマット
        # 睡眠管理用DataFrame
        df_sleepMan: DataFrame = pd.read_csv(
            path_sleepMan, header=0,
            parse_dates=['measurement_day'], date_format=FMT_DATE,
            usecols=SLEEP_MAN_COLS
        )
        # 頻尿要因用DataFrame
        df_noctFact: DataFrame = pd.read_csv(
            path_noctFact, header=0,
            parse_dates=['measurement_day'], date_format=FMT_DATE,
            usecols=NOCT_FACT_COLS
        )
  • 5-2. 2つのDataFrameをインデックス列 ('measurement_day') で結合する
        # INNER JOIN (1:1)
        df_sleepMan = df_sleepMan.set_index('measurement_day').join(
            df_noctFact.set_index('measurement_day')
        )
  • 6-1. 就寝時刻の計算 ※夜間トイレ回数プロット領域のX軸の列データの元データ
        days: Series = df_sleepMan.index
        bedTimes: List[Optional[datetime]] = [
            calcBedTime(
                day.strftime(FMT_DATE), wakeup, sleeping) for day, wakeup, sleeping in zip(
                days, df_sleepMan['wakeup_time'], df_sleepMan['sleeping_time']
            )
        ])
    
  • 6-2. Series.apply関数を使って列データを加工する
    • 起床時刻("%H:%M:%S")の秒部分をトリムする (【関数】trimSecondsWithTime) ※上段領域のX軸ラベル用に整形
    • 睡眠時間("%H:%M:%S")を分(整数)に変換 (【関数】toMinute)
    • 深い睡眠("%H:%M:%S")を分(整数)に変換 (【関数】toMinute)
        # 起床時刻("%H:%M:%S"): 秒部分をトリムする ※健康管理Androidアプリで入力した値が"%H:%M"
        df_sleepMan['wakeup_time'] = df_sleepMan['wakeup_time'].apply(trimSecondsWithTime)
        # 睡眠時間("%H:%M:%S"): 分(整数)に変換
        df_sleepMan['sleeping_time'] = df_sleepMan['sleeping_time'].apply(toMinute)
        # 深い睡眠("%H:%M:%S"): 分(整数)に変換
        df_sleepMan['deep_sleeping_time'] = df_sleepMan['deep_sleeping_time'].apply(toMinute)
  • 6.3 当該年月の期間に欠損データ (測定日未登録) があればインデックスを振り直す
    • 【単月データの場合】 データ件数 < 当該年月の日数
    • 【複数月に跨るデータの場合】 (インデックスの開始日〜終了日までの件数) < 当該年月の日数
        org_dataSize: int = df_sleepMan.index.shape[0]
        if org_dataSize < endDay:
            # 単月データで欠損値有り (測定日未登録)
            # https://pandas.pydata.org/docs/reference/api/pandas.date_range.html
            df_sleepMan = df_sleepMan.reindex(
                pd.date_range(start=start_date, end=end_date, name='measurement_day')
            )
        else:
            # 複数月にまたがるCSV
            # 月間データを取り出す
            df_sleepMan = df_sleepMan.loc[start_date:end_date]
            filtered_dataSize: int = df_sleepMan.shape[0]
            # 月間データに欠損データが有る場合は埋める
            if filtered_dataSize < endDay:
                df_sleepMan = df_sleepMan.reindex(
                    pd.date_range(start=start_date, end=end_date, name='measurement_day')
                )
(3) matplotlib描画に必要なデータとラベルなどの作成
    # ■(1) 深い睡眠データ
    deepSleepingSer: Series = df_sleepMan['deep_sleeping_time']
    # ■(2) 睡眠時間描画用の差分 ※積み上げ棒グラフの深い睡眠の上にスタック描画
    sleepingDiffSer: Series = df_sleepMan['sleeping_time'] - deepSleepingSer
    
    # データ件数(月間: 1〜末日までの日数)
    dateRangeSize: int = df_sleepMan.shape[0]
    # X軸のインデックス生成 ※月間の日数
    xIndexes = np.arange(dateRangeSize)
    # Seriesからプロット用ラベルデータを作成する 
    # メインプロットのX軸ラベル
    daySer: Series = df_sleepMamt-2 n.index
    # 起床時間の欠損値(測定日なし) NANをプランクを設定
    wakeupSer: Series = df_sleepMan['wakeup_time'].fillna("")
    # ■(L1) X軸ラベルリスト: "日 (曜日) " + 起床時刻
    xLabels: List[str] = [
        f"{makeDateLabel(day.strftime(FMT_DATE))} {wakeup}" for day, wakeup in zip(
            daySer, wakeupSer
        )
    ]
    # ■(L2) Y軸 (0〜12時間) ["00:00","00:30","01:00", ..., "11:30","12:00"]
    sleepingTimeYTicks: List = [minuteToFormatTime(x) for x in
                                range(0, SLEEP_TIME_MAX + 1, 30)]
    # 就寝時間: X軸出力用に時刻部分のみ設定する
    df_sleepMan['bed_time'] = [bedTm.strftime("%H:%M") for bedTm in bedTimes]

    # ■(4) 夜間トイレ回数 (散布図)
    toiletVisitsSer: Series = df_sleepMan['midnight_toilet_visits']
    # ■(L3) X軸に表示する就寝時間の欠損値は空文字を設定
    topXTicks: Series = df_sleepMan['bed_time'].fillna("")
2-2. matplotlibの処理コード
(1) 関数定義
  • 携帯用の描画領域サイズ(ピクセル)をインチに変換
    def pixelToInch(width_px: int, height_px: int, density: float) -> Tuple[float, float]:
        """
        携帯用の描画領域サイズ(ピクセル)をインチに変換する
        :param width_px: 幅(ピクセル)
        :param height_px: 高さ(ピクセル)
        :param density: 密度
        :return: 幅(インチ), 高さ(インチ)
        """
        px: float = 1 / rcParams["figure.dpi"]
        # ■ 複数の端末で試験した結果から導いた経験式
        px = px / (2.0 if density > 2.0 else density)
        inch_width = width_px * px
        inch_height = height_px * px
        return inch_width, inch_height
  • 睡眠スコア値出力とマーカー描画
    • 欠損データ(pd.isna(score))の場合は描画をスキップ
    • 睡眠スコアの値の範囲ごとにマーカーのスタイル(色)を変える
    • 折れ線グラフとしてプロット
    • 睡眠スコアはSeriesでは浮動小数点で格納されているため整数に整形して出力
    def drawScoreWithMarker(axes: matplotlib.pyplot.Axes, scoreSer: Series) -> None:
        """
        睡眠スコア値出力とマーカー描画
        (1)非常に良い (2)良い (3) (1),(2)以外に該当するスコア値とマーカー
        :param axes: 描画領域
        :param scoreSer: 睡眠スコアSeries(欠損データ[pd.na]有り)
        """
        for x_idx, score in enumerate(scoreSer):
            if pd.isna(score):  # pandasを使う場合 nan のチェックが必要
                # 欠損データはスキップ
                continue
    
            # マーカースタイル
            if score >= 100 * RATE_SCORE_BEST:
                scatter_style: Dict = SCATTER_SCORE_BEST_STYLE
            elif score >= 100 * RATE_SCORE_GOOD:
                scatter_style: Dict = SCATTER_SCORE_GOOD_STYLE
            elif score < 100 * RATE_SCORE_BAD:
                scatter_style: Dict = SCATTER_SCORE_BAD_STYLE
            else:
                scatter_style: Dict = SCATTER_SCORE_NORMAL_STYLE
            # マーカープロット
            axes.scatter(x_idx, score, **scatter_style)
            # 睡眠スコアは整数 ※Seriesでは浮動小数点で格納されているため整数に整形
            axes.text(x_idx, score + 1, f"{score:.0f}", **PLOT_TEXT_STYLE)
  • 睡眠スコアに応じた矩形領域を指定した背景色で描画
    def drawRectBackground(axes: matplotlib.pyplot.Axes,
                           y_pos_top: float, y_pos_bottom: float,
                           x_pos_start: float, x_pos_end: float,
                           facecolor: str, alpha: float = 0.2,
                           edgecolor: str = 'none') -> None:
        """
        睡眠スコアに応じた矩形領域を指定した背景色で描画
        :param axes: 描画領域
        :param y_pos_top: Y軸上端位置
        :param y_pos_bottom:  Y軸下端位置
        :param x_pos_start: X軸左端位置
        :param x_pos_end: X軸右端位置
        :param facecolor: 背景色
        :param alpha: アルファ値
        :param edgecolor: 矩形の線色
        """
        rect: Rectangle = Rectangle(
            xy=(x_pos_start, y_pos_bottom),
            width=(x_pos_end - x_pos_start), height=(y_pos_top - y_pos_bottom),
            facecolor=facecolor, edgecolor=edgecolor, alpha=alpha
        )
        axes.add_patch(rect)
    
(2) スクリプトメイン
  • 1-1. インポート
    import matplotlib
    import matplotlib.pyplot as plt
    import numpy as np
    from matplotlib import rcParams
    from matplotlib.axes import Axes
    from matplotlib.figure import Figure
    from matplotlib.patches import Rectangle
  • 1-2. 定数定義
    • ※1 Flaskアプリに組み込む予定なのでサイズ、フォントなどの体裁に関する変数は事前に定義する
    • ※2 スマートフォンの描画領域サイズは Flaskアプリではリクエストパラメーターから取得する
    # 棒グラフの幅倍率
    BAR_WIDTH: float = 0.7
    # 睡眠スコアの最大値
    SCORE_MAX: float = 100
    # 睡眠スコアステップ
    SCORE_STEP: float = 10
    # 睡眠時間(単位:分)の最小値
    SLEEP_TIME_MIN: int = 0
    # 睡眠時間(単位:分)の最大値: 12時間
    SLEEP_TIME_MAX: int = 12 * 60
    # Y軸(睡眠時間): 30分間隔で水平線を描画
    SLEEP_TIME_STEP: int = 30
    # X軸のマージン
    X_LIM_MARGIN: float = -0.5
    # 睡眠スコア(下限): 非常に良い (90〜100)
    RATE_SCORE_BEST: float = 0.9
    # 睡眠スコア(下限): 良い (80〜89)
    RATE_SCORE_GOOD: float = 0.8
    # 睡眠スコア(下限): やや低い (60〜79)
    # 睡眠スコア(下限): 低い (60未満)
    RATE_SCORE_BAD: float = 0.6
    # 睡眠スコアの背景色
    #  非常に良い (90〜100)
    COLOR_SCORE_BEST: str = 'gold'
    #  良い (80〜89)
    COLOR_SCORE_GOOD: str = 'lime'
    #  やや低い (60〜79)
    COLOR_SCORE_WORNING: str = 'red'
    #  低い (60未満)
    COLOR_SCORE_BAD: str = 'gray'
    # 睡眠スコアが基準値以上の場合に描画するマーカー色
    #   非常に良い(90)以上
    MARKER_COLOR_SCORE_BEST: str = 'red'
    #   良い(80)以上
    MARKER_COLOR_SCORE_GOOD: str = 'green'
    #   悪い
    MARKER_COLOR_SCORE_BAD: str = 'gray'
    # 折れ線の色: 睡眠スコア
    SCORE_LINE_COLOR: str = 'black'
    # 棒グラフの色: 睡眠時間
    COLOR_BAR_SLEEPING: str = 'gold'
    # 棒グラフの色: 深い睡眠時間
    COLOR_BAR_DEEP_SLEEPING: str = 'violet'
    # 凡例ラベル
    LABEL_SLEEPING: str = '睡眠時間 (時:分)'
    LABEL_DEEP_SLEEPING: str = '深い睡眠 (分)'
    LABEL_SLEEP_SCORE: str = '睡眠スコア'
    # 上端領域Y軸ラベル
    TOP_AXES_LABEL: str = '夜間トイレ回数'
    TOILET_VISITS_MIN: int = 0
    TOILET_VISITS_MAX: int = 6
    
    # スタイル辞書定数定義
    # 睡眠スコア折れ線グラフスタイル
    SCORE_LINE_STYLE: Dict = {'color': SCORE_LINE_COLOR, 'linewidth': 1.0}
    # 睡眠スコア折れ線グラフスタイル
    SCORE_TICKS_STYLE: Dict = {'color': SCORE_LINE_COLOR, 'fontsize': 9,
                                'fontweight': 'demibold'}
    # 棒グラフの外郭線スタイル
    BAR_LINE_STYLE: Dict = {'edgecolor': 'black', 'linewidth': 0.7}
    # X軸のラベル(日+曜日)スタイル
    X_TICKS_STYLE: Dict = {'fontsize': 9, 'fontweight': 'heavy', 'rotation': 90}
    # 上段: X軸のラベル(起床時間)スタイル
    TOP_X_TICKS_STYLE: Dict = {'fontsize': 8, 'fontweight': 'heavy', 'rotation': 90}
    # 棒グラフの上部に出力する睡眠時間(時:分)のフォントスタイル
    TIME_TICKS_STYLE: Dict = {'fontsize': 9}
    # 深い睡眠用(分)スタイル: 赤
    BAR_LABEL_STYLE: Dict = {'color': 'red', 'fontsize': 8, 'fontweight': 'heavy'}
    # 睡眠時間用(時:分)スタイル: 黒
    PLOT_TEXT_STYLE: Dict = {'fontsize': 8, 'fontweight': 'demibold',
                                'horizontalalignment': 'center', 'verticalalignment': 'bottom'}
    # タイトルフォントスタイル
    TITLE_FONT_DICT: Dict = {'fontsize': 10, 'fontweight': 'medium'}
    # スキャッターマーカースタイル
    MARKER_SIZE_WITH_MONTH: float = 9.
    # https://matplotlib.org/stable/gallery/shapes_and_collections/scatter.html
    # matplotlib.pyplot.scatter
    #  #sphx-glr-gallery-shapes-and-collections-scatter-py
    SCATTER_SCORE_BEST_STYLE: Dict = {
        'color': MARKER_COLOR_SCORE_BEST, 's': MARKER_SIZE_WITH_MONTH}
    SCATTER_SCORE_GOOD_STYLE: Dict = {
        'color': MARKER_COLOR_SCORE_GOOD, 's': MARKER_SIZE_WITH_MONTH}
    SCATTER_SCORE_NORMAL_STYLE: Dict = {
        'color': SCORE_LINE_COLOR, 's': MARKER_SIZE_WITH_MONTH}
    SCATTER_SCORE_BAD_STYLE: Dict = {
        'color': MARKER_COLOR_SCORE_BAD, 's': MARKER_SIZE_WITH_MONTH}
    # 上段: 夜間トイレ回数マーカースタイル ※一回り小さく
    SCATTER_TOILET_VISITS_STYLE: Dict = {'color': 'blue', 's': 8.}
    
    # 描画領域のグリッド線スタイル: Y方向のグリッド線のみ表示
    AXES_GRID_STYLE: Dict = {'axis': 'y', 'linestyle': 'dashed', 'linewidth': 0.7,
                                'alpha': 0.75}
    # 上段プロット領域:下段プロット領域比
    GRID_SPEC_HEIGHT_RATIO: List[int] = [1, 5]
    # 凡例位置 (上端,右側) ※睡眠スコア値が上端にプロットされることはまれのためプロットが隠れることが無い
    LEGEND_LOC: str = 'upper right'
    
    # スマートフォンの描画領域サイズ (ピクセル): Google pixel 4a
    PHONE_PX_WIDTH: int = 1064
    PHONE_PX_HEIGHT: int = 1704
    # 同上: 密度
    PHONE_DENSITY: float = 2.75
【サブプロット領域を上段・下段に作成】
    # グラフ出力
    # 携帯用の描画領域サイズ(ピクセル)をインチに変換
    fig_width_inch, fig_height_inch = pixelToInch(
        PHONE_PX_WIDTH, PHONE_PX_HEIGHT, PHONE_DENSITY
    )

    # 描画領域作成
    #  (1)上段描画領域: 夜間トイレ回数 (Y軸), 就寝時間 (X軸)
    #  (2)下段描画領域: 睡眠管理データ
    fig: Figure
    ax_top: Axes
    ax_main: Axes
    # GRID_SPEC_HEIGHT_RATIO = [1, 5]
    # 上段エリア (補助プロット): 1, 下段エリア (メインプロット): 5
    # 上段エリアのX軸に就寝時間を出力するため sharex=False (デフォルト) とする
    fig, (ax_top, ax_main) = plt.subplots(
        2, 1, gridspec_kw={'height_ratios': GRID_SPEC_HEIGHT_RATIO}, layout='constrained',
        figsize=(fig_width_inch, fig_height_inch)
    )
    # Y方向のグリッド線のみ表示
    ax_main.grid(**AXES_GRID_STYLE)
    ax_top.grid(**AXES_GRID_STYLE)
【睡眠管理データプロット領域】
    # 下段メインプロット領域
    # 深い睡眠: 棒グラフ
    ax_main.bar(xIndexes, deepSleepingSer, BAR_WIDTH,
                color=COLOR_BAR_DEEP_SLEEPING,
                label=LABEL_DEEP_SLEEPING, **BAR_LINE_STYLE)
    # 睡眠時間 (深い睡眠との差分): 棒グラフ
    ax_main.bar(xIndexes, sleepingDiffSer, BAR_WIDTH,
                color=COLOR_BAR_SLEEPING,
                bottom=deepSleepingSer,
                label=LABEL_SLEEPING, **BAR_LINE_STYLE)
    # 凡例の位置設定
    ax_main.legend(loc=LEGEND_LOC)
    ax_main.set_ylabel("睡眠時間")
    # y軸ラベル: 睡眠時間 "時:分"
    ax_main.set_yticks(np.arange(SLEEP_TIME_MIN, (SLEEP_TIME_MAX + 1), SLEEP_TIME_STEP),
                       sleepingTimeYTicks,
                       **TIME_TICKS_STYLE)
    ax_main.set_ylim(SLEEP_TIME_MIN, SLEEP_TIME_MAX)
    # x軸ラベル
    ax_main.set_xticks(xIndexes, xLabels, **X_TICKS_STYLE)
    ax_main.set_xlim(X_LIM_MARGIN, dateRangeSize + X_LIM_MARGIN)

    # 睡眠スコアを取得: 折れ線グラフ (ラベル軸は右側)
    sleepScoreSer: Series = df_sleepMan['sleep_score']
    # 右側に軸を作成
    ax_main_score = ax_main.twinx()
    ax_main_score.set_ylabel(LABEL_SLEEP_SCORE)
    ax_main_score.plot(xIndexes, sleepScoreSer, **SCORE_LINE_STYLE)
    # 右側y軸ラベル: 100まで表示させるため+1
    ax_main_score.set_yticks(np.arange(0, (SCORE_MAX + 1), SCORE_STEP),
                             np.arange(0, (SCORE_MAX + 1), SCORE_STEP),
                             **SCORE_TICKS_STYLE)
    # 右側Y軸値(0〜100)
    ax_main_score.set_ylim(0, SCORE_MAX)
    # 睡眠スコアが良い以上の場合はスコア値を表示
    drawScoreWithMarker(ax_main_score, sleepScoreSer)
【睡眠スコア範囲の矩形 (4段階) 】
    # 睡眠スコア範囲の矩形描画
    # 非常に良い
    drawRectBackground(ax_main, SLEEP_TIME_MAX,
                       SLEEP_TIME_MAX * RATE_SCORE_BEST,
                       X_LIM_MARGIN, dateRangeSize + X_LIM_MARGIN,
                       facecolor=COLOR_SCORE_BEST)
    # 良い
    drawRectBackground(ax_main, SLEEP_TIME_MAX * RATE_SCORE_BEST,
                       SLEEP_TIME_MAX * RATE_SCORE_GOOD,
                       X_LIM_MARGIN, dateRangeSize + X_LIM_MARGIN,
                       facecolor=COLOR_SCORE_GOOD)
    # やや低い
    drawRectBackground(ax_main, SLEEP_TIME_MAX * RATE_SCORE_GOOD,
                       SLEEP_TIME_MAX * RATE_SCORE_BAD,
                       X_LIM_MARGIN, dateRangeSize + X_LIM_MARGIN,
                       facecolor=COLOR_SCORE_WORNING, alpha=0.1)
    # 低い
    drawRectBackground(ax_main, SLEEP_TIME_MAX * RATE_SCORE_BAD,
                       SLEEP_TIME_MIN,
                       X_LIM_MARGIN, dateRangeSize + X_LIM_MARGIN,
                       facecolor=COLOR_SCORE_BAD, alpha=0.1)
【夜間トイレ回数プロット領域】
    # 上端プロット領域
    # タイトル
    ax_top.set_title(titleDateRange)
    # 夜間トイレ回数 (散布図)
    toiletVisitsSer: Series = df_sleepMan['midnight_toilet_visits']
    # X軸に表示する就寝時間の欠損値は空文字を設定
    topXTicks: Series = df_sleepMan['bed_time'].fillna("")
    ax_top.scatter(xIndexes, toiletVisitsSer, **SCATTER_TOILET_VISITS_STYLE)
    ax_top.set_ylim(TOILET_VISITS_MIN, TOILET_VISITS_MAX)
    ax_top.set_ylabel(TOP_AXES_LABEL)
    ax_top.set_yticks(range(TOILET_VISITS_MIN, TOILET_VISITS_MAX + 1))
    # 睡眠時間をX軸に表示 ※X軸数はメインプロット領域と同一
    ax_top.set_xlim(X_LIM_MARGIN, dateRangeSize + X_LIM_MARGIN)
    ax_top.set_xticks(xIndexes, topXTicks, **TOP_X_TICKS_STYLE)
3. コマンドラインからスクリプト実行
  • Python仮想環境に切り替える
    $ . py_venv/py_healthcare_tool/bin/activate
  • 実行 ※見やすいよう改行をいれていますが1行です
    (py_healthcare_tool) $ python PlotSleepManBar2Plot_3_pandas_month.py
        --year-month 2023-03
        --sleep-man datas/csv/sleep_management_202303.csv
        --noct-fact datas/csv/nocturia_factors_202303.csv
【実行ログ】
INFO Namespace(year_month='2023-03', sleep_man='datas/csv/sleep_management_202303.csv', 
    noct_fact='datas/csv/nocturia_factors_202303.csv')
INFO (26, 5)
INFO                 wakeup_time  sleep_score  sleeping_time  deep_sleeping_time  midnight_toilet_visits bed_time
measurement_day                                                                                             
2023-03-01            05:30         83.0            360                50.0                       0    23:30
2023-03-02            05:30         79.0            400                50.0                       3    22:50
2023-03-03            05:30         93.0            470                80.0                       1    21:40
2023-03-04            06:15         83.0            420                60.0                       2    23:15
2023-03-06            05:30         84.0            430                80.0                       4    22:20
2023-03-10            05:30         70.0            400                50.0                       1    22:50
2023-03-11            06:10         76.0            300                50.0                       2    01:10
2023-03-13            05:30         81.0            410                50.0                       2    22:40
2023-03-14            05:30         86.0            410                50.0                       1    22:40
2023-03-15            05:30         68.0            320                50.0                       4    00:10
2023-03-16            05:30         86.0            390                50.0                       1    23:00
2023-03-17            05:30         84.0            380                50.0                       1    23:10
2023-03-18            06:20         68.0            380                40.0                       5    00:00
2023-03-19            05:30         64.0            320                30.0                       2    00:10
2023-03-20            05:20         84.0            380                70.0                       2    23:00
2023-03-21            05:15          NaN            340                 NaN                       2    23:35
2023-03-22            05:30         71.0            330                40.0                       2    00:00
2023-03-23            05:00         80.0            360                50.0                       2    23:00
2023-03-24            05:30         71.0            370                20.0                       1    23:20
2023-03-25            06:15         76.0            410                50.0                       3    23:25
2023-03-26            06:15         77.0            430                40.0                       2    23:05
2023-03-27            05:00         78.0            480                60.0                       4    21:00
2023-03-28            05:00         79.0            370                50.0                       2    22:50
2023-03-29            05:00         71.0            300                50.0                       2    00:00
2023-03-30            05:10         90.0            380                70.0                       5    22:50
2023-03-31            04:50         82.0            360                40.0                       1    22:50
INFO DatetimeIndex(['2023-03-01', '2023-03-02', '2023-03-03', '2023-03-04',
              '2023-03-06', '2023-03-10', '2023-03-11', '2023-03-13',
              '2023-03-14', '2023-03-15', '2023-03-16', '2023-03-17',
              '2023-03-18', '2023-03-19', '2023-03-20', '2023-03-21',
              '2023-03-22', '2023-03-23', '2023-03-24', '2023-03-25',
              '2023-03-26', '2023-03-27', '2023-03-28', '2023-03-29',
              '2023-03-30', '2023-03-31'],
             dtype='datetime64[ns]', name='measurement_day', freq=None)
INFO org_dataSize:26
INFO 26 < endDay: 31
INFO (31, 6)
INFO                 wakeup_time  sleep_score  sleeping_time  deep_sleeping_time  midnight_toilet_visits bed_time
measurement_day                                                                                             
2023-03-01            05:30         83.0          360.0                50.0                     0.0    23:30
2023-03-02            05:30         79.0          400.0                50.0                     3.0    22:50
2023-03-03            05:30         93.0          470.0                80.0                     1.0    21:40
2023-03-04            06:15         83.0          420.0                60.0                     2.0    23:15
2023-03-05              NaN          NaN            NaN                 NaN                     NaN      NaN
2023-03-06            05:30         84.0          430.0                80.0                     4.0    22:20
2023-03-07              NaN          NaN            NaN                 NaN                     NaN      NaN
2023-03-08              NaN          NaN            NaN                 NaN                     NaN      NaN
2023-03-09              NaN          NaN            NaN                 NaN                     NaN      NaN
2023-03-10            05:30         70.0          400.0                50.0                     1.0    22:50
2023-03-11            06:10         76.0          300.0                50.0                     2.0    01:10
2023-03-12              NaN          NaN            NaN                 NaN                     NaN      NaN
2023-03-13            05:30         81.0          410.0                50.0                     2.0    22:40
2023-03-14            05:30         86.0          410.0                50.0                     1.0    22:40
2023-03-15            05:30         68.0          320.0                50.0                     4.0    00:10
2023-03-16            05:30         86.0          390.0                50.0                     1.0    23:00
2023-03-17            05:30         84.0          380.0                50.0                     1.0    23:10
2023-03-18            06:20         68.0          380.0                40.0                     5.0    00:00
2023-03-19            05:30         64.0          320.0                30.0                     2.0    00:10
2023-03-20            05:20         84.0          380.0                70.0                     2.0    23:00
2023-03-21            05:15          NaN          340.0                 NaN                     2.0    23:35
2023-03-22            05:30         71.0          330.0                40.0                     2.0    00:00
2023-03-23            05:00         80.0          360.0                50.0                     2.0    23:00
2023-03-24            05:30         71.0          370.0                20.0                     1.0    23:20
2023-03-25            06:15         76.0          410.0                50.0                     3.0    23:25
2023-03-26            06:15         77.0          430.0                40.0                     2.0    23:05
2023-03-27            05:00         78.0          480.0                60.0                     4.0    21:00
2023-03-28            05:00         79.0          370.0                50.0                     2.0    22:50
2023-03-29            05:00         71.0          300.0                50.0                     2.0    00:00
2023-03-30            05:10         90.0          380.0                70.0                     5.0    22:50
2023-03-31            04:50         82.0          360.0                40.0                     1.0    22:50
INFO sleepingDiff:
measurement_day
2023-03-01    310.0
2023-03-02    350.0
2023-03-03    390.0
2023-03-04    360.0
2023-03-05      NaN
2023-03-06    350.0
2023-03-07      NaN
2023-03-08      NaN
2023-03-09      NaN
2023-03-10    350.0
2023-03-11    250.0
2023-03-12      NaN
2023-03-13    360.0
2023-03-14    360.0
2023-03-15    270.0
2023-03-16    340.0
2023-03-17    330.0
2023-03-18    340.0
2023-03-19    290.0
2023-03-20    310.0
2023-03-21      NaN
2023-03-22    290.0
2023-03-23    310.0
2023-03-24    350.0
2023-03-25    360.0
2023-03-26    390.0
2023-03-27    420.0
2023-03-28    320.0
2023-03-29    250.0
2023-03-30    310.0
2023-03-31    320.0
Freq: D, dtype: float64
INFO figure.dpi[px]: 0.01
INFO px[2.75]: 0.005
INFO fig_width_inch: 5.32, fig_height_inch: 8.52
INFO fig: Figure(532x852), ax_top: Axes(0.125,0.763333;0.775x0.116667), ax_main: Axes(0.125,0.11;0.775x0.583333)
INFO screen_shots/PlotSleepManBar2Plot_3_pandas_month.png
【欠損データ有りの出力結果】
リポジトリに戻る
https://github.com/pipito-yukio/matplotlib_knowhow