3. データ変換#
これまでのノートブックでは、マークや視覚エンコーディングを使用して個々のデータレコードを表現する方法を学びました。ここでは、複数のレコードを要約するための集計の使用を含む、データの変換方法を探ります。データ変換は可視化の重要な一部です。表示する変数の選択や詳細レベルの設定は、適切な視覚エンコーディングを選ぶことと同じくらい重要です。どんなにエンコーディングが適切であっても、誤った情報を表示していては意味がありません!
このモジュールを進める際には、Altair Data Transformationsドキュメントを別のタブで開いておくことをお勧めします。詳細が必要な場合や、他の利用可能な変換を確認したい場合に役立つリソースです。
このノートブックは、データ可視化カリキュラム の一部です。
import pandas as pd
import altair as alt
3.1. 映画に関するデータセット#
今回扱うのは、vega-datasetsコレクションから取得した映画に関するデータテーブルです。このデータには、映画名、監督、ジャンル、公開日、評価、総収入などの変数が含まれています。ただし、このデータを扱う際には注意が必要です。映画は不均一にサンプリングされた年から選ばれており、複数のソースから統合されたデータを使用しています。掘り下げていくと、欠損値や微妙なエラーが見つかることもあります!それでも、このデータは探索する価値があるでしょう。
まず、vega_datasets
パッケージからJSONデータファイルのURLを取得し、Pandasデータフレームに読み込んでその内容を確認してみましょう。
movies_url = 'https://cdn.jsdelivr.net/npm/vega-datasets@1/data/movies.json'
movies = pd.read_json(movies_url)
映画データセットには、何行(レコード)と何列(フィールド)が含まれていますか?
movies.shape
(3201, 16)
それでは、最初の5行を確認して、フィールドとデータ型の概要を把握してみましょう…
movies.head(5)
Title | US_Gross | Worldwide_Gross | US_DVD_Sales | Production_Budget | Release_Date | MPAA_Rating | Running_Time_min | Distributor | Source | Major_Genre | Creative_Type | Director | Rotten_Tomatoes_Rating | IMDB_Rating | IMDB_Votes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | The Land Girls | 146083.0 | 146083.0 | NaN | 8000000.0 | Jun 12 1998 | R | NaN | Gramercy | None | None | None | None | NaN | 6.1 | 1071.0 |
1 | First Love, Last Rites | 10876.0 | 10876.0 | NaN | 300000.0 | Aug 07 1998 | R | NaN | Strand | None | Drama | None | None | NaN | 6.9 | 207.0 |
2 | I Married a Strange Person | 203134.0 | 203134.0 | NaN | 250000.0 | Aug 28 1998 | None | NaN | Lionsgate | None | Comedy | None | None | NaN | 6.8 | 865.0 |
3 | Let's Talk About Sex | 373615.0 | 373615.0 | NaN | 300000.0 | Sep 11 1998 | None | NaN | Fine Line | None | Comedy | None | None | 13.0 | NaN | NaN |
4 | Slam | 1009819.0 | 1087521.0 | NaN | 1000000.0 | Oct 09 1998 | R | NaN | Trimark | Original Screenplay | Drama | Contemporary Fiction | None | 62.0 | 3.4 | 165.0 |
3.2. ヒストグラム#
変換の探索を始めるにあたり、データを離散的なグループに_ビン分割_し、それらのグループを要約するためにレコードを_カウント_します。このようにして得られるプロットは、ヒストグラムとして知られています。
まずは未集計データを見てみましょう。映画の評価について、Rotten Tomatoesの評価とIMDBユーザーの評価を比較した散布図を作成します。Altairにデータを提供するには、映画データのURLをChart
メソッドに渡します。(Pandasデータフレームを直接渡しても同じ結果が得られます。)その後、Rotten TomatoesとIMDBの評価フィールドをx
とy
チャネルでエンコードします:
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q'),
alt.Y('IMDB_Rating:Q')
)
このデータを要約するために、データフィールドをビン分割して数値を離散的なグループに分類することができます。ここでは、x
エンコーディングチャネルにbin=True
を追加することでx軸に沿ってビン分割を行います。その結果、10ポイントごとに等しいステップサイズで分割された10個のビンが生成され、それぞれが10点の評価範囲に対応します。
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=True),
alt.Y('IMDB_Rating:Q')
)
bin=True
はデフォルトのビン設定を使用しますが、必要に応じてさらに制御することもできます。ここでは、最大ビン数(maxbins
)を20に設定してみましょう。この設定により、ビンの数が倍増し、各ビンが5点の評価範囲に対応するようになります。
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('IMDB_Rating:Q')
)
データをビン分割した後、Rotten Tomatoesの評価分布を要約してみましょう。ここではIMDBの評価を一旦省略し、代わりにy
エンコーディングチャネルを使用してレコードの集計count
を表示します。この場合、各ビンに含まれる映画の数を示す垂直位置がプロットされます。
count
集計はフィールド値に関係なく各ビン内の総レコード数を数えるため、y
エンコーディングでフィールド名を指定する必要はありません。
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('count()')
)
標準的なヒストグラムを作成するために、マークタイプをcircle
からbar
に変更しましょう:
alt.Chart(movies_url).mark_bar().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('count()')
)
これで評価の分布をより明確に確認できます。ネガティブな評価の方が少なく、高評価の方がやや多いものの、全体的には概ね均一な分布が見られます。Rotten Tomatoesの評価は、映画評論家による「賛成」または「反対」の判断を基に、ポジティブなレビューの割合を計算して決定されます。この方法は、評価値の全範囲をうまく活用しているように見えます。
同様に、x
エンコーディングチャネルのフィールドを変更することで、IMDB評価のヒストグラムを作成できます。
alt.Chart(movies_url).mark_bar().encode(
alt.X('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('count()')
)
先ほど見た比較的均一な分布とは対照的に、IMDBの評価はベル型(ただし負の歪み)の分布を示しています。IMDBの評価は、サイトのユーザーが提供するスコア(1から10の範囲)を平均して形成されます。この測定方法が、Rotten Tomatoesの評価とは異なる分布の形状をもたらしていることがわかります。また、分布のモード(最頻値)が6.5から7の間にあることも確認できます。人々は一般的に映画を楽しむ傾向があり、そのためポジティブなバイアスが生じているのかもしれません!
それでは、Rotten TomatoesとIMDBの評価の散布図に戻り、元のプロットで両方の軸をビン分割した場合を見てみましょう。
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
)
オーバープロットにより詳細が失われており、多くのポイントが互いに重なって描画されています。
2次元のヒストグラムを作成するには、以前と同様にcount
集計を追加します。ただし、x
とy
エンコーディングチャネルはすでに使用されているため、count
を表現するには別のエンコーディングチャネルを使用する必要があります。ここでは、sizeエンコーディングチャネルを追加して円の面積でカウントを表現した結果を示します。
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Size('count()')
)
また、color
チャネルを使用してカウントをエンコードし、マークタイプをbar
に変更することもできます。その結果、ヒートマップ形式の2次元ヒストグラムが得られます。
alt.Chart(movies_url).mark_bar().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Color('count()')
)
上記のサイズベースと色ベースの2Dヒストグラムを比較してみましょう。どちらのエンコーディングが優れていると思いますか?その理由は何でしょうか?以下の視点から考えてみてください。個々の値の大きさをより正確に比較できるプロットはどれですか?全体的な評価の密度をより正確に見ることができるプロットはどれですか?
3.3. 集計#
カウントは集計の一種に過ぎません。他にも、average
(平均)、median
(中央値)、min
(最小値)、max
(最大値)などの指標を使用して要約を計算することができます。Altairのドキュメントには、利用可能な集計関数の完全なセットが記載されています。
いくつかの例を見てみましょう!
3.3.1. 平均値とソート#
映画のジャンルによって批評家からの評価に一貫した違いがあるのでしょうか? この質問に答えるための第一歩として、各ジャンルの映画について平均値(別名: 算術平均値)の評価を調べてみましょう。
ジャンルをy
軸に、Rotten Tomatoesの評価のaverage
(平均)をx
軸にプロットして視覚化します。
alt.Chart(movies_url).mark_bar().encode(
alt.X('average(Rotten_Tomatoes_Rating):Q'),
alt.Y('Major_Genre:N')
)
興味深い変動が見られるようですが、データをアルファベット順にリストとして表示するだけでは、ジャンルに対する批評家の反応をランク付けするにはあまり役立ちません。
より分かりやすい可視化のために、平均評価の降順でジャンルをソートしてみましょう。そのためには、y
エンコーディングチャネルにsort
パラメータを追加し、平均値(op
で集計操作を指定)Rotten Tomatoes評価(field
)を降順
(order
)でソートするよう指定します。
alt.Chart(movies_url).mark_bar().encode(
alt.X('average(Rotten_Tomatoes_Rating):Q'),
alt.Y('Major_Genre:N', sort=alt.EncodingSortField(
op='average', field='Rotten_Tomatoes_Rating', order='descending')
)
)
このソートされたプロットからは、批評家がドキュメンタリー、ミュージカル、西部劇、ドラマを高く評価している一方で、ロマンチックコメディやホラー映画には低評価を与えていることがわかります… そして、誰が「null
映画」を愛さないわけがあるでしょうか!?
3.3.2. 中央値と四分位範囲#
平均値はデータを要約する一般的な方法ですが、場合によっては誤解を招くことがあります。例えば、非常に大きな値や小さな値(外れ値)が平均値を歪める可能性があります。これを避けるために、ジャンルを中央値の評価で比較することも考えられます。
中央値はデータを均等に分割するポイントであり、半分の値が中央値より小さく、もう半分が中央値より大きくなります。中央値は外れ値に対して影響を受けにくいため、ロバストな統計量と呼ばれます。例えば、最大の評価値を恣意的に増加させても、中央値には影響を与えません。
プロットを更新して、median
集計を使用し、その値でソートしてみましょう。
alt.Chart(movies_url).mark_bar().encode(
alt.X('median(Rotten_Tomatoes_Rating):Q'),
alt.Y('Major_Genre:N', sort=alt.EncodingSortField(
op='median', field='Rotten_Tomatoes_Rating', order='descending')
)
)
平均が近いジャンルの中で順位が入れ替わったものも見られます(ジャンル不明の映画、つまりnull
が最も高く評価されています!)。それでも、全体的なグループは安定しています。ホラー映画は依然としてプロの映画評論家からあまり支持されていません。
集計統計を見る際には、懐疑的な目を持つことが大切です。これまで私たちは点推定のみを見てきましたが、ジャンル内で評価がどのように変動しているかはまだ調べていません。
ランキングにニュアンスを加えるために、ジャンル間の評価の変動を可視化してみましょう。ここでは、各ジャンルの四分位範囲(IQR)をエンコードします。IQRはデータ値の中央の半分が存在する範囲です。四分位数はデータ値の25%を含み、四分位範囲は中央の50%を含む2つの中間四分位数から構成されます。
範囲を可視化するには、x
とx2
エンコーディングチャネルを使用して開始点と終了点を示します。集計関数としてq1
(下位四分位境界)とq3
(上位四分位境界)を使用して四分位範囲を提供します。(ちなみに、q2は中央値を表します。)
alt.Chart(movies_url).mark_bar().encode(
alt.X('q1(Rotten_Tomatoes_Rating):Q'),
alt.X2('q3(Rotten_Tomatoes_Rating):Q'),
alt.Y('Major_Genre:N', sort=alt.EncodingSortField(
op='median', field='Rotten_Tomatoes_Rating', order='descending')
)
)
3.3.3. 時間単位#
次に全く異なる質問をしてみましょう:興行収入は季節によって変動するのでしょうか?
この質問に初歩的に答えるために、月ごとのアメリカ国内の興行収入中央値をプロットしてみます。
このチャートを作成するには、timeUnit
変換を使用して公開日を1年のmonth
にマッピングします。この結果はビン分割に似ていますが、意味のある時間間隔を使用します。他に有効な時間単位には、year
、quarter
、date
(月の日付)、day
(曜日)、hours
、および複合単位(yearmonth
やhoursminutes
など)があります。Altairドキュメントで時間単位の完全なリストを確認してください。
alt.Chart(movies_url).mark_area().encode(
alt.X('month(Release_Date):T'),
alt.Y('median(US_Gross):Q')
)
結果のプロットを見ると、アメリカ国内の映画の中央値の売上は、夏の大作シーズンや年末のホリデー期間にピークを迎えるようです。もちろん、映画に出かけるのはアメリカだけでなく世界中の人々です。全世界の総興行収入でも同様のパターンが見られるのでしょうか?
alt.Chart(movies_url).mark_area().encode(
alt.X('month(Release_Date):T'),
alt.Y('median(Worldwide_Gross):Q')
)
Yes!
3.4. 高度なデータ変換#
上記の例では、すべてエンコーディングチャネルに関連付けられた変換(bin、timeUnit、aggregate、sort)を使用しています。しかし、可視化の前に複数の変換を連鎖的に適用したり、エンコーディング定義に組み込まれない変換を使用したい場合があります。そのような場合、AltairやVega-Liteではエンコーディングとは別に定義されたデータ変換をサポートしています。これらの変換は、エンコーディングが考慮される前にデータに適用されます。
データ変換はPandasを使用して直接行い、その結果を可視化することも可能です。しかし、組み込みの変換を使用すると、可視化を他のコンテキストでより簡単に公開できるようになります。たとえば、Vega-Lite JSONをエクスポートして、スタンドアロンのウェブインターフェースで使用することができます。Altairがサポートする組み込みの変換(calculate
、filter
、aggregate
、window
など)を見ていきましょう。
3.4.1. 計算(Calculate)#
アメリカ国内の興行収入と全世界の興行収入の比較を思い出してください。全世界の収入にはアメリカが含まれていませんか?(実際に含まれています。)アメリカ以外のトレンドをよりよく理解するにはどうすればよいでしょうか?
calculate
変換を使用すると、新しいフィールドを派生させることができます。ここでは、全世界の興行収入からアメリカ国内の興行収入を差し引きたいと考えています。calculate
変換はVegaの式文字列を受け取り、単一のレコードに対する数式を定義します。Vegaの式ではJavaScriptの構文を使用します。datum.
プレフィックスを使用して入力レコードのフィールド値にアクセスします。
alt.Chart(movies).mark_area().transform_calculate(
NonUS_Gross='datum.Worldwide_Gross - datum.US_Gross'
).encode(
alt.X('month(Release_Date):T'),
alt.Y('median(NonUS_Gross):Q')
)
アメリカ国外でも季節的なトレンドが維持されていることがわかりますが、ピークでない月にはより顕著な減少が見られます。
3.4.2. フィルター(Filter)#
filter変換は、元のデータの部分集合を持つ新しいテーブルを作成し、指定された述語テストを満たさない行を削除します。calculate変換と同様に、フィルタ述語はVega式言語を使用して表現されます。
以下では、IMDBとRotten Tomatoesの評価を比較する初期の散布図にフィルターを追加し、「Romantic Comedy」という主要ジャンルの映画のみに限定しています。
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q'),
alt.Y('IMDB_Rating:Q')
).transform_filter('datum.Major_Genre == "Romantic Comedy"')
他のジャンルを表示するようにフィルターを変更すると、プロットはどのように変化するでしょうか?フィルター式を編集して確認してみましょう。
次に、1970年以前に公開された映画を表示するようフィルターを設定してみましょう。
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q'),
alt.Y('IMDB_Rating:Q')
).transform_filter('year(datum.Release_Date) < 1970')
これらの映画は異常に高い評価を受けているようです!古い映画は単に質が良いのでしょうか?それとも、このデータセットにおいて、高評価の古い映画が選ばれる選択バイアスが存在しているのでしょうか?
3.4.3. 集計(Aggregate)#
これまでにエンコーディングチャネルのコンテキストで、count
やaverage
といったaggregate
変換を見てきました。aggregate
は、他の変換(以下のwindow
変換の例など)の前処理ステップとして、別途指定することも可能です。aggregate
変換の出力は、新しいデータテーブルであり、このテーブルにはgroupby
フィールドと計算されたaggregate
指標が含まれます。
ジャンルごとの平均評価のプロットを再作成してみましょう。ただし、今回は個別のaggregate
変換を使用します。aggregate
変換の出力テーブルには13行が含まれ、それぞれがジャンル1つに対応します。
y
軸をソートするには、ソート指示に必須の集計操作を含める必要があります。ここでは、max
演算子を使用します。ジャンルごとに出力レコードが1つだけであるため、max
は問題なく機能します。同様にmin
演算子を使用しても、同じプロットが得られます。
alt.Chart(movies_url).mark_bar().transform_aggregate(
groupby=['Major_Genre'],
Average_Rating='average(Rotten_Tomatoes_Rating)'
).encode(
alt.X('Average_Rating:Q'),
alt.Y('Major_Genre:N', sort=alt.EncodingSortField(
op='max', field='Average_Rating', order='descending'
)
)
)
3.4.4. ウィンドウ(Window)#
window
変換は、データレコードのソートされたグループに対して計算を行います。window
変換は非常に強力で、ランキング、リード/ラグ分析、累積合計、移動和や移動平均などのタスクをサポートします。window
変換によって計算された値は、新しいフィールドとして元のデータテーブルに書き戻されます。window
操作には、これまで見てきた集計操作のほか、rank
、row_number
、lead
、lag
などの特殊な操作が含まれます。利用可能なすべてのウィンドウ操作については、Vega-Liteのドキュメントを参照してください。
window
変換のユースケースの1つは、トップkリストの計算です。ここでは、総全世界興行収入に基づいてトップ20の監督をプロットしてみましょう。
まず、監督が不明なレコードを除外するためにfilter
変換を使用します。そうしないと、null
監督がリストを支配してしまいます!次に、監督ごとにグループ化して全映画の全世界興行収入を合計するためのaggregate
を適用します。この時点でソートされた棒グラフをプロットすることは可能ですが、何百人もの監督が表示されてしまいます。これをトップ20に限定するにはどうすれば良いでしょうか?
window
変換を使用すると、ソート順に基づいてランキングを計算することでトップ監督を特定できます。window
変換の定義内で、興行収入でsort
を行い、そのソート順に基づいてrank
操作でランクスコアを計算します。その後、ランク値が20以下のレコードのみをデータに残すために、続くfilter
変換を追加します。
alt.Chart(movies_url).mark_bar().transform_filter(
'datum.Director != null'
).transform_aggregate(
Gross='sum(Worldwide_Gross)',
groupby=['Director']
).transform_window(
Rank='rank()',
sort=[alt.SortField('Gross', order='descending')]
).transform_filter(
'datum.Rank < 20'
).encode(
alt.X('Gross:Q'),
alt.Y('Director:N', sort=alt.EncodingSortField(
op='max', field='Gross', order='descending'
))
)
スティーブン・スピルバーグが非常に成功したキャリアを持っていることがわかります!しかし、合計値を表示すると、キャリアが長く、映画を多く作り、その結果多くの収益を上げた監督が有利になります。集計操作の選択を変更した場合どうなるでしょうか?たとえば、平均
や中央値
の興行収入では、最も成功した監督は誰になるでしょうか?上記のaggregate
変換を変更してみましょう!
このノートブックの前半でヒストグラムについて見てきましたが、これは値の集合の確率密度関数を近似します。それに対する補完的なアプローチとして累積分布を調べる方法があります。例えば、各ビンが自分のカウントだけでなく、すべての前のビンのカウントも含むようなヒストグラムを考えてみてください。結果として_累積合計_が得られ、最後のビンにはレコードの総数が含まれます。累積チャートでは、特定の参照値について、それ以下のデータ値がいくつあるかを直接示します。
具体的な例として、上映時間(分単位)ごとの映画の累積分布を見てみましょう。上映時間の情報を含むレコードは一部のみであるため、まず上映時間を持つ映画のサブセットにfilter
を適用します。次に、aggregate
を使用して上映時間ごとに映画の数をカウントします(暗黙的に1分ごとの「ビン」を使用)。最後に、window
変換を使用して、上映時間の増加順にソートされたビン全体でカウントの累積合計を計算します。
alt.Chart(movies_url).mark_line(interpolate='step-before').transform_filter(
'datum.Running_Time_min != null'
).transform_aggregate(
groupby=['Running_Time_min'],
Count='count()',
).transform_window(
Cumulative_Sum='sum(Count)',
sort=[alt.SortField('Running_Time_min', order='ascending')]
).encode(
alt.X('Running_Time_min:Q', axis=alt.Axis(title='Duration (min)')),
alt.Y('Cumulative_Sum:Q', axis=alt.Axis(title='Cumulative Count of Films'))
)
映画の長さの累積分布を調べてみましょう。上映時間が110分未満の映画が、上映時間データを持つ全映画の約半分を占めていることがわかります。90分から2時間の間で映画が着実に累積していき、その後分布が減少し始めます。珍しいことに、このデータセットには3時間を超える映画も複数含まれています!
3.5. まとめ#
データ変換が可能なことのほんの一部を見てきました!利用可能なすべての変換とそのパラメータを含む詳細については、Altairのデータ変換ドキュメントをご覧ください。
場合によっては、可視化ツールを使用する前に、データを準備するために大規模なデータ変換が必要になることがあります。Python内でデータ加工(データラングリング)を行うには、Pandasライブラリを使用することができます。