🦢

よく使うpandasを使った集計や統計まとめ

に公開

今回も備忘録です。

0. はじめに

  • pandasは、データの読み込み・加工・集計・可視化 を簡単にするためのPythonライブラリ
  • 1時間ごとの平均値や、カテゴリごとの統計値を、メソッドや関数1行でできたりする(超便利!)
  • 今回は、私が個人的によく使う関数やメソッドを紹介しています

0.1 PC環境など

  • OS : Windows 11 Home
  • python : 3.13.5
    • pandas : 2.3.3
    • numpy : 2.3.5

0.2 今回使用するデータ

…といってもメソッドの紹介等を普通にしていたら、公式ドキュメントの劣化記事にしかならない
というわけで今回は、気象庁のデータを使用して、実際に私が遭遇したエラーおよび原因分析、解決法なども含めて、データ分析・統計解析・加工を行う一連の流れを紹介してみました

本記事で使用するデータ(data.csv)は、以下の条件で抽出をしております

  • 地点:愛知 -> 名古屋
  • 項目:日平均気温、日最高気温、日最低気温
  • 期間:2024/1/1~2024/12/31

出典:気象庁ホームページ ( https://www.data.jma.go.jp/risk/obsdl/index.php

また、適時プログラムによる加工を行っております。ご了承ください

1. データの読み込み

  • まずはpythonで本データを扱うにはcsvファイルの読み込みが必要
  • pandasの場合は、Excel等の表形式データはDataFrame型として読み込むことが多いです

1.1 1回目

まずは、普通にread_csvで読み込んでみましょう

import pandas as pd

df_data = pd.read_csv('./data.csv')

出力

エラーが出力されましたー
…まぁ、1発で読み込めないことなんてよくあるので、エラー内容を確認してみましょう

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x83 in position 0: invalid start byte

原因

「UnicodeDecodeError」なので、おそらく読み込み時に指定した文字コードが間違っているようです
まぁ、"utf-8"じゃなければ"shift-jis"でしょうと推測
ちなみにpd.read_csvのデフォルトではencoding="utf-8"で読み込みをするため、何も指定していない場合、上記エラーが発生しやすいです

1.2 2回目

encoding="shift-jis"で読み込み時の文字コードをshift-jisにして再度実行

df_data = pd.read_csv('./data.csv', encoding="shift-jis")

出力

読み込めたけど、思った結果と違う!!

原因

実際に「data.csv」をメモ帳で開いてみる

「「「でたな! なんちゃってcsvファイル!!!」」」

やっぱりcsvといえど、データの中身はちゃんと確認しておくべきでした
(いつからcsvファイルが "(カンマ)区切りの(お行儀のよい)表形式"だと錯覚していた...?)

というわけで、メモ帳で開いて以下の部分を目視で確認します

  1. columns(列名)として読み込みたい行が、何行目であるか? 確認
    -> 4行目を、DataFrameの列名(columns)として読み込みたい
  2. ,で区切られた実際の表形式のデータが、何行目から始まっているか? の確認
    -> 7行目から、dataとして読み込みたい

とゆーわけで、1を考慮して、skiprows=3(3行飛ばして4行目を列名として読み込み)、読み込み後、ilocで先頭2行※を切り出すようにします
※2行 = 7(表形式データの始まり行)ー4(skiprowsの分)ー1(columnsとして読み込まれる分)

1.3 3回目

これでどうかなー

df_data = pd.read_csv('./data.csv', skiprows=3, encoding="shift-jis")
df_data = df_data.iloc[2:]  # 最初の2行をスキップ

出力

これで、ひとまず表形式として読み込むことができました
ふぅ…ここまでくると一安心ですね

2. データ分析

さて、読み込んだ後は、実際にデータを分析してみましょう!

2.1 describesで統計値のサマリー確認

まず、ざっくり欠損値や外れ値がないかを確認してみます
describesは、各列ごとに、平均・標準偏差・最小値・1Q(25%パーセンタイル)・2Q(median)・3Q・最大値 をいっぺんに算出してくれます(便利!)

print("=== describes ===\n", df_data.describe())

出力

出力はこんな感じ~

この出力からわかることとして

  • countは3行とも366で、最初のDataFrameの行数と一致している
    -> 欠損値Nanや空白はなさそう
  • min, maxの値から極端な外れ値はなさそう(日本の気候の常識の範囲内)
    -> 気温70度超えなど明らかな間違いの値などはなさそう

といった内容を確認してました
今回は幸いにも、欠損値が"-"(ハイフン文字)や"なし"等で記載されてなくてよかったです
あったら前処理として、where等で置換処理を入れないといけないですからねー

2.2 1カ月毎の平均値の算出(1回目)

なんか、上司が1カ月毎の平均をまとめてと命令が来たとします(適当)
Excelでポチポチしてもいいですが、resampleとmean使えば1行です(チェーンメソッド使用)

print("=== resample ===\n", df_data.resample('1ME').mean())

出力

なんかエラー吐きました、ぴえん

TypeError: Only valid with DatetimeIndex, TimedeltaIndex or PeriodIndex, but got an instance of 'RangeIndex'

原因

IndexがDatetimeIndex, TimedeltaIndex or PeriodIndexでないと、月毎とかの日付ベースの集計はできないよ ってエラーが言ってます。

2.2 1カ月毎の平均値の算出(2回目)

というわけで、indexの型を確認してみます(print文)

print(df_data.index)

出力

RangeIndex(start=2, stop=368, step=1)

あー、そもそも 列:年月日 をindexにしてないのね、理解
resampleはindexをベースに集計を行うので、read_csvの時点でindexとして指定しておきましょう

修正案

read_csvのキーワード引数(index_colとparse_datesを指定)を修正

  • index_col:Indexとして使用する列名を指定
  • parse_dates:Timestamp(日付)として読み込む列名を指定
    (Trueの場合はIndexのみを日時として読み込む)
df_data = pd.read_csv('./data.csv', skiprows=3, encoding="shift-jis", index_col="年月日", parse_dates=True)
print(df_data)
print(df_data.index)

出力

これで、Indexが年月日(一番左の列に1段下がって表示されている) かつ
DatetimeIndex(日付の型)になっていることが確認できました

2.3 1カ月毎の平均値の算出(2回目)

再度実行(忘れてきたのでプログラム全文記載)

df_data = pd.read_csv('./data.csv', skiprows=3, encoding="shift-jis", index_col="年月日", parse_dates=True)
df_data = df_data.iloc[2:]  # 最初の2行をスキップ
print("=== resample ===\n", df_data.resample('1ME').mean())

出力

エラー吐きました。 吐きたいのはこっちだよ~(笑)

TypeError: agg function failed [how->mean,dtype->object]

原因

まぁ、出力の通り、agg function(この場合はmean()が該当)でdtypeがobjectだから平均できませんってエラー
わかりやすかったから吐かずにすみました

確認してみる

print(df_data.dtypes)

出力

平均気温(℃)      float64
平均気温(℃).1     object
平均気温(℃).2     object
最高気温(℃)      float64
最高気温(℃).1     object
最高気温(℃).2     object
最低気温(℃)      float64
最低気温(℃).1     object
最低気温(℃).2     object
dtype: object

あー、一部object型(数値以外も格納できる型)として読み込まれている列がありますね~
個別に愚直に変換しますかー

追加したコード

df_data["平均気温(℃).1"] = df_data["平均気温(℃).1"].astype("float64")
df_data["平均気温(℃).2"] = df_data["平均気温(℃).2"].astype("float64")

df_data["最高気温(℃).1"] = df_data["最高気温(℃).1"].astype("float64")
df_data["最高気温(℃).2"] = df_data["最高気温(℃).2"].astype("float64")

df_data["最低気温(℃).1"] = df_data["最低気温(℃).1"].astype("float64")
df_data["最低気温(℃).2"] = df_data["最低気温(℃).2"].astype("float64")

ちなみに、read_csvの時点でdtypeを指定する方法も試しましたが、
先頭2行に文字列("品質情報")があるため、float型に変換できません(エラーになる)
つまり、そいつらがいるので、read_csvは泣く泣くobject型として読んでいたわけですね…
pandasはこういった"おせっかい"を裏で行っていたりするので、挙動を経験的に理解しておくことも重要です

2.4 1カ月毎の平均値の算出(3回目)

これでどうだ!?

df_data = pd.read_csv('./data.csv', skiprows=3, encoding="shift-jis", index_col="年月df_data = df_data.iloc[2:]  # 最初の2行をスキップ

# 型変換
df_data["平均気温(℃).1"] = df_data["平均気温(℃).1"].astype("float64")
df_data["平均気温(℃).2"] = df_data["平均気温(℃).2"].astype("float64")
df_data["最高気温(℃).1"] = df_data["最高気温(℃).1"].astype("float64")
df_data["最高気温(℃).2"] = df_data["最高気温(℃).2"].astype("float64")
df_data["最低気温(℃).1"] = df_data["最低気温(℃).1"].astype("float64")
df_data["最低気温(℃).2"] = df_data["最低気温(℃).2"].astype("float64")

print("=== resample ===\n", df_data.resample('1ME').mean())

出力

これでOKだね、やった~

3. 直近7日間の移動平均の算出

えっ? 曜日ごとに変動があるかもだから7日間で移動平均を出してほしい?
そんな売店の売り上げ分析じゃないんだから…と思いつつ一応やってみる。
ちなみに移動平均は、rollingを使えば簡単に実装可能(件数ベース、日付ベース両方対応できる!)

print("=== rolling ===\n", df_data.rolling('7D').mean())  # 7Day毎の移動平均を算出

出力(DataFrame)

ちなみに、特徴量の計算等でも移動平均はよく使いますが、列指定してSeriesに適用することの方が多いです(これは個人的な感想)

print("=== rolling ===\n", df_data["平均気温(℃)"].rolling('7D').mean())  # 7Day毎の移動平均を算出

出力(Series)

4. 差分(diff)

え、平均気温の前日差分が見たい、なんで?

4.1 温度の前日差分

(まぁ、1行でできるからいいか...)

print("=== diff ===\n", df_data["平均気温(℃)"].diff(periods=1))  # 差分を算出

出力

ちなみに、以下のプログラムと同じ結果になります

頑張ってる実装
list_ = [None]
for i in range(1, len(df_data)):
    diff = df_data["平均気温(℃)"].iat[i] - df_data["平均気温(℃)"].iat[i-1]
    list_.append( diff )
df_data["平均気温(℃)"] = list_

ちょっと賢い実装

df_data["平均気温(℃)"][1:] = df_data["平均気温(℃)"][1:].to_numpy() - df_data["平均気温(℃)"][:-1].to_numpy()
df_data["平均気温(℃)"][0] = None

ちなみに、periods=2とすれば、2つ前との差分なども算出可能なので、上記2つのプログラムの出番はないと思います。

5. グループ化(groupby)

え、月ごとの平均値が見たい?

5.1 月ごとの集計

以下の戦略で実装します

  • 集計用の列(Month)を追加(indexから月の数値のみ追加)
  • Month列を指定して、groupbyで集計
df_data["Month"] = df_data.index.month  # 年月日の月のみ抽出して別の列として格納
print(df_data.groupby("Month").mean())

出力

6. ピボットテーブル(pivot)

列を月に、行を日 にした革新的なマトリクスの表が欲しい?
キミ、それ、pivot_tableって名前あるのよ…

6.1 ピボットテーブル

以下を指定すればpivotで一発です(ちなみにpivot_tableもあるよ!)

df_data["Month"] = df_data.index.month  # 年月日の月のみ抽出して別の列として格納
df_data["Day"] = df_data.index.day  # 年月日の日のみ抽出して別の列として格納
print(df_data.pivot(index="Month", columns="Day", values="平均気温(℃)"))

出力

7. 欠損値の確認(isna)

え、pivot_tableにnanがあったので欠損値がないか確認しなさい~?
(pivotで2024/2/31などない部分がnanが格納されているだけなんだけどなぁ...)

7.1 欠損値の件数の確認

「0」(欠損値がない)という数字を出したいので、nan(欠損値)の件数をカウントして出力します

print(df_data.isna())
print(df_data.isna().sum(axis=0))  # sumではTrueは1, Falseは0として集計される

出力

  • isnaをつかえば、nanである要素の位置がTrue, それ以外はFalseのDataFrameが得られる
  • それを縦方向(axis=0)で合計すれば、欠損値の件数が算出可能

はい、0件ですね。

8. 条件式による置換(where)

え、偶数月と奇数月で分けて傾向が見たい?

8.1 pandasのwhereを使う場合

pandas(DataFrame)のwhereは、Trueの場合のみを置換するため、
False部分を書き換える場合は、反対の条件でもう一回実行する必要がある

tmp = df_data["Month"].where(df_data["Month"]%2 == 0, "Even")  # 条件式がTrueの値のみ書き換え、Falseの要素はそのまま
tmp = tmp.where(df_data["Month"]%2 != 0, "Odd")  # tmpはSeriesなので、列名の参照は不要
df_data["Month_cat"] = tmp
print(df_data)

出力

一番左に列:Month_catとして偶数月には"Even"、奇数月には"Odd"をいれときましたよー

8.2 numpyのwhereを使う場合

nunpy(np.where)の場合は、Trueの場合とFalseの場合を1行で置換できる

import numpy as np
tmp = np.where(df_data["Month"]%2 == 0, "Even", "Odd")  # np.whereなら条件式のTrue, False両方の書き換えが1行で可能
df_data["Month_cat"] = tmp
print(df_data)

出力

9. 空白のDataFrameの判定(empty)

え、幻の13月がないか確認しろ?(ついにぼけたのかしら...?)

9.1 query -> emptyで判定

基本、if文の条件式として使うことが多いかと思います。
今回はmonth=13のDataFrameをqueryで抽出した結果、0件(empty)だった場合で判定してます

month_13 = df_data.query(" Month == 13")
print(month_13)
if month_13.empty:
    print("13月なんて存在しないです!")
else:
    print("13月ありました、すみませんでした!!!")

出力

empty(要素が空)であるDataFrameをprintすると、以下のように"Empty DataFrame"と表示されますが、これを判定するために、emptyを用いましょう

まとめ

  • データ分析・加工でよく使うメソッドや関数を、実際の要求とともに書き並べてみた
  • こういった実際のデータ処理について、きれいに記載してる記事が多い印象だったので、今回はあえてエラーと試行錯誤をすべて記載してみた
  • 実際、データ分析にかぎらずプログラミングは試行錯誤が必要なことが、読者に伝われば幸いです

Discussion