よく使う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ファイルが "(カンマ)区切りの(お行儀のよい)表形式"だと錯覚していた...?)
というわけで、メモ帳で開いて以下の部分を目視で確認します
- columns(列名)として読み込みたい行が、何行目であるか? 確認
-> 4行目を、DataFrameの列名(columns)として読み込みたい - ,で区切られた実際の表形式のデータが、何行目から始まっているか? の確認
-> 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.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