Tidyデータ#

Tidyデータは、データ分析と視覚化を容易にするために、データを規則的な形式に整える方法です。以下の原則に基づいています:

  1. 各列が変数を表す:データセット内の各列が特定の変数を表します。たとえば、気温データセットでは、各列が異なる地点や異なる日時の気温を表します。

  2. 各行が観測値を表す:データセット内の各行が個々の観測値を表します。気温データセットでは、各行が特定の地点と日時における気温の観測値を表します。

  3. 各セルが単一の値を表す:各セルには単一の値が含まれます。つまり、セルに複数の値が含まれていないことが重要です。

Table 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

このデータセットでは、年ごとにケースと人口の列が分かれています。これでは解析が難しく、変数と観測値が明確に分かれていません。

Table 2 Tidyデータの例#

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ライブラリ)で簡単に処理できます。

  • 一貫性:データの一貫性が保たれるため、エラーの発生率が低くなります。

  • 再利用性:データが整理されているため、再利用が容易で、他のプロジェクトや研究で使いやすくなります。

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']
shape: (5, 60)
countryiso2iso3yearnew_sp_m014new_sp_m1524new_sp_m2534new_sp_m3544new_sp_m4554new_sp_m5564new_sp_m65new_sp_f014new_sp_f1524new_sp_f2534new_sp_f3544new_sp_f4554new_sp_f5564new_sp_f65new_sn_m014new_sn_m1524new_sn_m2534new_sn_m3544new_sn_m4554new_sn_m5564new_sn_m65new_sn_f014new_sn_f1524new_sn_f2534new_sn_f3544new_sn_f4554new_sn_f5564new_sn_f65new_ep_m014new_ep_m1524new_ep_m2534new_ep_m3544new_ep_m4554new_ep_m5564new_ep_m65new_ep_f014new_ep_f1524new_ep_f2534new_ep_f3544new_ep_f4554new_ep_f5564new_ep_f65newrel_m014newrel_m1524newrel_m2534newrel_m3544newrel_m4554newrel_m5564newrel_m65newrel_f014newrel_f1524newrel_f2534newrel_f3544newrel_f4554newrel_f5564newrel_f65
strstrstri64strstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstr
"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()
shape: (5, 6)
countryiso2iso3yearkeycases
strstrstri64stri32
"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
shape: (76_046, 8)
countryiso2iso3yeartypesexagecases
strstrstri64strstrstri32
"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

年齢列変換#

最後に次のコードで、年齢範囲を表す列を年齢開始列と年齢終了列に変換します。

  1. まずは、一貫性を保つために、replace()"014""0014" に、"65""65NA" に置き換えます。つまり、すべての値の先頭2文字が開始年齢、後ろ2文字が終了年齢になります。この操作は2回使うので、変数 age に保存されます。

  2. 次に、先頭2文字を取り出し、UInt8 にキャストします。

  3. 最後に、後ろ2文字を取り出し、strict=FalseUInt8 にキャストします。

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')
)
shape: (76_046, 2)
age_startage_end
u8u8
014
014
014
014
014
65null
65null
65null
65null
65null

纏め#

上のプログラムを次のようにまとめます。処理速度を向上させるため、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
shape: (76_046, 7)
countryyeartypesexcasesage_startage_end
stri64strstri64u8u8
"Afghanistan"1997"sp""m"0014
"Afghanistan"1998"sp""m"30014
"Afghanistan"1999"sp""m"8014
"Afghanistan"2000"sp""m"52014
"Afghanistan"2001"sp""m"129014
"Viet Nam"2013"rel""f"311065null
"Wallis and Futuna Islands"2013"rel""f"265null
"Yemen"2013"rel""f"36065null
"Zambia"2013"rel""f"66965null
"Zimbabwe"2013"rel""f"72565null