Tidy データ#
Tidy データは、データ分析と可視化をしやすくするために、データを決まった形に整える方法です。次の原則に基づきます。
各列が変数を表す — データセットの各列が1つの変数に対応します。たとえば気温のデータセットでは、列が地点や日時の気温を表します。
各行が観測値を表す — 各行が1つの観測値に対応します。気温のデータセットでは、行が特定の地点・日時の観測値を表します。
各セルが単一の値を表す — 各セルには1つの値だけが入ります。
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 |
| "Lithuania" | "LT" | "LTU" | 2013 | "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" | "19" | "49" | "150" | "254" | "299" | "209" | "160" | "18" | "42" | "76" | "84" | "82" | "57" | "74" |
| "Bhutan" | "BT" | "BTN" | 1986 | "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" |
| "Colombia" | "CO" | "COL" | 1993 | "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" |
| "British Virgin Islands" | "VG" | "VGB" | 2004 | "NA" | "NA" | "NA" | "NA" | "1" | "NA" | "NA" | "NA" | "NA" | "1" | "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" |
| "Spain" | "ES" | "ESP" | 2012 | "10" | "112" | "259" | "299" | "276" | "156" | "220" | "15" | "101" | "202" | "161" | "70" | "24" | "74" | "125" | "92" | "172" | "175" | "166" | "133" | "297" | "120" | "72" | "141" | "118" | "70" | "50" | "114" | "35" | "71" | "164" | "162" | "104" | "85" | "196" | "38" | "71" | "138" | "140" | "75" | "54" | "173" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" | "NA" |
Tidy データに変更#
典型的な実データの例で、冗長な列や奇妙な変数コード、多くの欠損値が含まれています。データが散らかっていて、整えるには複数の手順が必要です。Polars の関数は1つのことをうまくこなすように設計されているため、実際には複数の操作をパイプラインでつなぐことになります。
国(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
<>:6: SyntaxWarning: "\w" is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\w"? A raw string is also an option.
<>:6: SyntaxWarning: "\w" is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\w"? A raw string is also an option.
C:\Users\ruoyu\AppData\Local\Temp\ipykernel_13676\3199331167.py:6: SyntaxWarning: "\w" is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\w"? A raw string is also an option.
.str.extract_groups('^new_(?<type>\w+)_(?<sex>\w)(?<age>\d+)$')
| 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にキャストします。
警告
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"),
)
df5
| 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 |