Tidyデータ#
Tidyデータは、データ分析と視覚化を容易にするために、データを規則的な形式に整える方法です。以下の原則に基づいています:
各列が変数を表す:データセット内の各列が特定の変数を表します。たとえば、気温データセットでは、各列が異なる地点や異なる日時の気温を表します。
各行が観測値を表す:データセット内の各行が個々の観測値を表します。気温データセットでは、各行が特定の地点と日時における気温の観測値を表します。
各セルが単一の値を表す:各セルには単一の値が含まれます。つまり、セルに複数の値が含まれていないことが重要です。
Country |
1999_cases |
1999_population |
2000_cases |
2000_population |
---|---|---|---|---|
Afghanistan |
745 |
19987071 |
2666 |
20595360 |
Brazil |
37737 |
172006362 |
80488 |
174504898 |
China |
212258 |
1272915272 |
213766 |
1280428583 |
このデータセットでは、年ごとにケースと人口の列が分かれています。これでは解析が難しく、変数と観測値が明確に分かれていません。
Country |
Year |
Cases |
Population |
---|---|---|---|
Afghanistan |
1999 |
745 |
19987071 |
Afghanistan |
2000 |
2666 |
20595360 |
Brazil |
1999 |
37737 |
172006362 |
Brazil |
2000 |
80488 |
174504898 |
China |
1999 |
212258 |
1272915272 |
China |
2000 |
213766 |
1280428583 |
この形式では、各列が変数を表し、各行が観測値を表し、各セルが単一の値を持っています。このように整理されたデータは、分析や視覚化に適しています。
Tidyデータの利点
操作が容易:tidyデータは、データ操作や変換のためのツール(例えば、RのtidyverseパッケージやPythonのpandasライブラリ)で簡単に処理できます。
一貫性:データの一貫性が保たれるため、エラーの発生率が低くなります。
再利用性:データが整理されているため、再利用が容易で、他のプロジェクトや研究で使いやすくなります。
サンプルデータ: tidyverse/tidyr
import polars as pl
from polars import selectors as cs
ファイル読み込み#
who.csv
ファイルには、年、国、年齢、性別、および診断方法別に分類された結核(TB)症例が含まれています。このデータセットには豊富な疫学情報が含まれていますが、提供されている形式でデータを扱うのは困難です。
次のコードでwho.csv
を読み込み、列名とランダムに5行のデータを観察します。
df = pl.read_csv('data/who.csv')
print(df.columns)
df.sample(5)
['country', 'iso2', 'iso3', 'year', 'new_sp_m014', 'new_sp_m1524', 'new_sp_m2534', 'new_sp_m3544', 'new_sp_m4554', 'new_sp_m5564', 'new_sp_m65', 'new_sp_f014', 'new_sp_f1524', 'new_sp_f2534', 'new_sp_f3544', 'new_sp_f4554', 'new_sp_f5564', 'new_sp_f65', 'new_sn_m014', 'new_sn_m1524', 'new_sn_m2534', 'new_sn_m3544', 'new_sn_m4554', 'new_sn_m5564', 'new_sn_m65', 'new_sn_f014', 'new_sn_f1524', 'new_sn_f2534', 'new_sn_f3544', 'new_sn_f4554', 'new_sn_f5564', 'new_sn_f65', 'new_ep_m014', 'new_ep_m1524', 'new_ep_m2534', 'new_ep_m3544', 'new_ep_m4554', 'new_ep_m5564', 'new_ep_m65', 'new_ep_f014', 'new_ep_f1524', 'new_ep_f2534', 'new_ep_f3544', 'new_ep_f4554', 'new_ep_f5564', 'new_ep_f65', 'newrel_m014', 'newrel_m1524', 'newrel_m2534', 'newrel_m3544', 'newrel_m4554', 'newrel_m5564', 'newrel_m65', 'newrel_f014', 'newrel_f1524', 'newrel_f2534', 'newrel_f3544', 'newrel_f4554', 'newrel_f5564', 'newrel_f65']
country | iso2 | iso3 | year | new_sp_m014 | new_sp_m1524 | new_sp_m2534 | new_sp_m3544 | new_sp_m4554 | new_sp_m5564 | new_sp_m65 | new_sp_f014 | new_sp_f1524 | new_sp_f2534 | new_sp_f3544 | new_sp_f4554 | new_sp_f5564 | new_sp_f65 | new_sn_m014 | new_sn_m1524 | new_sn_m2534 | new_sn_m3544 | new_sn_m4554 | new_sn_m5564 | new_sn_m65 | new_sn_f014 | new_sn_f1524 | new_sn_f2534 | new_sn_f3544 | new_sn_f4554 | new_sn_f5564 | new_sn_f65 | new_ep_m014 | new_ep_m1524 | new_ep_m2534 | new_ep_m3544 | new_ep_m4554 | new_ep_m5564 | new_ep_m65 | new_ep_f014 | new_ep_f1524 | new_ep_f2534 | new_ep_f3544 | new_ep_f4554 | new_ep_f5564 | new_ep_f65 | newrel_m014 | newrel_m1524 | newrel_m2534 | newrel_m3544 | newrel_m4554 | newrel_m5564 | newrel_m65 | newrel_f014 | newrel_f1524 | newrel_f2534 | newrel_f3544 | newrel_f4554 | newrel_f5564 | newrel_f65 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
str | str | str | i64 | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str | str |
"Angola" | "AO" | "AGO" | 1984 | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" |
"Paraguay" | "PY" | "PRY" | 1988 | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" |
"Mali" | "ML" | "MLI" | 1985 | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" |
"United Republic of Tanzania" | "TZ" | "TZA" | 2005 | "190" | "2062" | "4939" | "4025" | "2310" | "1279" | "1054" | "271" | "1852" | "3521" | "1892" | "968" | "547" | "354" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" |
"Australia" | "AU" | "AUS" | 1989 | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" |
Tidyデータに変更#
これは非常に典型的な実データセットの例です。冗長な列や奇妙な変数コード、多くの欠損値が含まれています。要するに、データが散らかっており、整頓するためには複数のステップが必要です。Polarsは各関数が一つのことをうまく行うように設計されています。つまり、実際の状況では通常、複数の操作をパイプラインとしてつなげる必要があります。
国(country)、ISO2(iso2)、ISO3(iso3)は、国を冗長に指定している3つの変数です。年(year)も明らかに変数です。他の列が何を示しているのかはまだ分かりませんが、変数名の構造(例えば、new_sp_m014、new_ep_m014、new_ep_f014)から判断すると、これらは変数ではなく値である可能性が高いです。
又、これらの列は”NA”でNULLを表す整数型のデータだと分かります。次のコードでこれらの列を整数型に変換します。strict
引数がFalse
の場合は、変換できない値をNULLに置き換えます。
variable_cols = ['country', 'iso2', 'iso3', 'year']
df2 = df.with_columns(
pl.exclude(variable_cols)
.cast(pl.Int32, strict=False)
)
次のコードで、new_sp_m014
からnewrel_f65
までのすべての列を集めます。それらの値が何を表しているのかはまだ分からないので、これらに”key”という一般的な名前を付けます。セルが症例の数を表していることは分かっているので、変数名には”cases”を使用します。多くの欠損値があるため、drop_nulls()
で存在する値に焦点を当てます。
df3 = (
df2
.unpivot(
index=variable_cols,
variable_name="key",
value_name="cases"
)
.drop_nulls('cases')
)
df3.head()
country | iso2 | iso3 | year | key | cases |
---|---|---|---|---|---|
str | str | str | i64 | str | i32 |
"Afghanistan" | "AF" | "AFG" | 1997 | "new_sp_m014" | 0 |
"Afghanistan" | "AF" | "AFG" | 1998 | "new_sp_m014" | 30 |
"Afghanistan" | "AF" | "AFG" | 1999 | "new_sp_m014" | 8 |
"Afghanistan" | "AF" | "AFG" | 2000 | "new_sp_m014" | 52 |
"Afghanistan" | "AF" | "AFG" | 2001 | "new_sp_m014" | 129 |
key列の情報取り出し#
key
列の値について、以下のフォーマットです。
最初の3文字は、その列に新しい結核症例が含まれているか古い症例が含まれているかを示しています。このデータセットでは、各列には新しい症例が含まれています。
次の文字は結核のタイプを示しています:
rel
は再発症例を表しますep
は肺外結核症例を表しますsn
は肺スメア(喀痰検査)で診断できなかった肺結核症例(スメア陰性)を表しますsp
は肺スメア(喀痰検査)で診断できた肺結核症例(スメア陽性)を表します
次の文字は結核患者の性別を示しています。このデータセットでは、男性(m
)と女性(f
)で症例をグループ化しています。
残りの数字は年齢層を示しています。このデータセットでは、症例を以下の7つの年齢層にグループ化しています:
014
= 0 – 14 歳1524
= 15 – 24 歳2534
= 25 – 34 歳3544
= 35 – 44 歳4554
= 45 – 54 歳5564
= 55 – 64 歳65
= 65 歳以上
列名の形式を少し修正する必要があります。残念ながら、名前がわずかに不一致であるためです。例えば、new_rel
の代わりに newrel
があります(ここでは分かりにくいですが、修正しないと次のステップでエラーが発生します)。
.str.replace()
を使って文字列を置換することを学びますが、基本的な考え方は非常に簡単です:文字列 “newrel” を “new_rel” に置き換えます。これにより、すべての変数名が一貫性を持つようになります。.str.extract_groups()
で正規表現にマッチングしたパターンを抽出し、抽出した情報は構造体列に保存されています。unnest()
で構造体列のフィールドを普通の列に変更します。
df4 = (
df3
.with_columns(
pl.col('key')
.str.replace("newrel", "new_rel")
.str.extract_groups('^new_(?<type>\w+)_(?<sex>\w)(?<age>\d+)$')
)
.unnest('key')
)
df4
country | iso2 | iso3 | year | type | sex | age | cases |
---|---|---|---|---|---|---|---|
str | str | str | i64 | str | str | str | i32 |
"Afghanistan" | "AF" | "AFG" | 1997 | "sp" | "m" | "014" | 0 |
"Afghanistan" | "AF" | "AFG" | 1998 | "sp" | "m" | "014" | 30 |
"Afghanistan" | "AF" | "AFG" | 1999 | "sp" | "m" | "014" | 8 |
"Afghanistan" | "AF" | "AFG" | 2000 | "sp" | "m" | "014" | 52 |
"Afghanistan" | "AF" | "AFG" | 2001 | "sp" | "m" | "014" | 129 |
… | … | … | … | … | … | … | … |
"Viet Nam" | "VN" | "VNM" | 2013 | "rel" | "f" | "65" | 3110 |
"Wallis and Futuna Islands" | "WF" | "WLF" | 2013 | "rel" | "f" | "65" | 2 |
"Yemen" | "YE" | "YEM" | 2013 | "rel" | "f" | "65" | 360 |
"Zambia" | "ZM" | "ZMB" | 2013 | "rel" | "f" | "65" | 669 |
"Zimbabwe" | "ZW" | "ZWE" | 2013 | "rel" | "f" | "65" | 725 |
年齢列変換#
最後に次のコードで、年齢範囲を表す列を年齢開始列と年齢終了列に変換します。
まずは、一貫性を保つために、
replace()
で"014"
を"0014"
に、"65"
を"65NA"
に置き換えます。つまり、すべての値の先頭2文字が開始年齢、後ろ2文字が終了年齢になります。この操作は2回使うので、変数age
に保存されます。次に、先頭2文字を取り出し、
UInt8
にキャストします。最後に、後ろ2文字を取り出し、
strict=False
でUInt8
にキャストします。
Warning
replace()
と .str.replace()
の区別:replace()
は値全体を別の値に置き換えますが、.str.replace()
は文字列の一部を別の文字列に置き換えます。
age = pl.col('age').replace(["014", "65"], ["0014", "65NA"])
df5 = (
df4
.select(
age
.str.slice(0, 2)
.cast(pl.UInt8)
.alias('age_start'),
age
.str.slice(2)
.cast(pl.UInt8, strict=False)
.alias('age_end')
)
.drop('iso2', 'iso3', 'age')
)
age_start | age_end |
---|---|
u8 | u8 |
0 | 14 |
0 | 14 |
0 | 14 |
0 | 14 |
0 | 14 |
… | … |
65 | null |
65 | null |
65 | null |
65 | null |
65 | null |
纏め#
上のプログラムを次のようにまとめます。処理速度を向上させるため、scan_csv()
で遅延計算のデータフレームを取得し、一連の操作の最後に collect()
で結果を収集します。
CSVファイルを読み込む際に、null_values
引数でNULLの文字列を指定することで、new*
列はすべて整数型になります。
age = pl.col('age').replace(["014", "65"], ["0014", "65NA"])
variable_cols = ['country', 'iso2', 'iso3', 'year']
df = (
pl.scan_csv('data/who.csv', null_values=['NA'])
.unpivot(
index=variable_cols,
variable_name="key",
value_name="cases"
)
.drop_nulls('cases')
.with_columns(
pl.col('key')
.str.replace("newrel", "new_rel")
.str.extract_groups('^new_(?<type>\\w+)_(?<sex>\\w)(?<age>\\d+)$')
)
.unnest('key')
.with_columns(
age
.str.slice(0, 2)
.cast(pl.UInt8)
.alias('age_start'),
age
.str.slice(2)
.cast(pl.UInt8, strict=False)
.alias('age_end')
)
.drop('iso2', 'iso3', 'age')
.collect()
)
df
country | year | type | sex | cases | age_start | age_end |
---|---|---|---|---|---|---|
str | i64 | str | str | i64 | u8 | u8 |
"Afghanistan" | 1997 | "sp" | "m" | 0 | 0 | 14 |
"Afghanistan" | 1998 | "sp" | "m" | 30 | 0 | 14 |
"Afghanistan" | 1999 | "sp" | "m" | 8 | 0 | 14 |
"Afghanistan" | 2000 | "sp" | "m" | 52 | 0 | 14 |
"Afghanistan" | 2001 | "sp" | "m" | 129 | 0 | 14 |
… | … | … | … | … | … | … |
"Viet Nam" | 2013 | "rel" | "f" | 3110 | 65 | null |
"Wallis and Futuna Islands" | 2013 | "rel" | "f" | 2 | 65 | null |
"Yemen" | 2013 | "rel" | "f" | 360 | 65 | null |
"Zambia" | 2013 | "rel" | "f" | 669 | 65 | null |
"Zimbabwe" | 2013 | "rel" | "f" | 725 | 65 | null |