σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft Academyロードマップ
Ctrl K
AO
+1.2k 人の学習者
ログイン参加する ↗
ロードマップ
AO
+1.2k 人の学習者
ログイン
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python ロードマップ›モジュール 10 · 高性能Pythonエンジニアリング›トピック 10.12·~50 分上級

ParquetとArrow

ロードマップ上の位置モジュール 10/11 · トピック 12/13
10.12 ParquetとArrow
—ラボスコア— / 100合計XP—連続日数—

行か列か

CSV ファイルや行指向のデータベースは、レコードを1件ずつまとめて保存します。約定1のすべてのフィールド、次に約定2、という具合です。分析ではそれはほとんど望まれません。1000万件の約定の平均価格を求めるのに必要なのは price 列だけですが、行のレイアウトでは、読み手が他のフィールドをすべて飛ばして進まなければなりません。列指向のレイアウトは各列を連続して保持するので、クエリは指名した列だけを読み、似た値が隣り合うためよく圧縮され、列に対する算術は1つの密なメモリブロックの上で走ります。だからクオンツのデータレイクはディスク上で Parquet、メモリ上で Arrow を使います。

# The same table as rows and as columns
rows = [{"symbol": "AAPL", "price": 184.25, "qty": 100},
        {"symbol": "MSFT", "price": 410.5, "qty": 50},
        {"symbol": "AAPL", "price": 184.5, "qty": 25}]

columns = {name: [row[name] for row in rows] for name in rows[0]}
print(columns)                                       # {'symbol': ['AAPL', 'MSFT', 'AAPL'], 'price': [184.25, 410.5, 184.5], 'qty': [100, 50, 25]}
print(sum(columns["price"]) / len(rows))             # 259.75   the average only touched one column

back = [{name: columns[name][i] for name in columns} for i in range(len(rows))]
print(back == rows)                                  # True  nothing is lost either way

Parquet:行グループ、統計、エンコーディング

Parquet ファイルはテーブルを行グループ(たとえば各100万行)に分け、グループ内では各列が独立したチャンクとして保存され、ファイルは各チャンクの最小値と最大値を記録します。price が180から190の範囲のクエリは、まず統計を読み、その範囲に当てはまる価格を含みえない行グループをすべて読み飛ばし(述語プッシュダウン)、要求された列だけを読みます(射影プッシュダウン)。列はエンコードもされます。銘柄のように異なる値が少ない列は、小さな辞書と行ごとの小さな整数コードとして保存され、その後に圧縮されます。繰り返しの多い市場データのファイルは、CSV の10分の1の大きさになることがよくあります。

# Dictionary encoding turns a few distinct values into a small dictionary plus integer codes
def dictionary_encode(values):
    positions, dictionary, codes = {}, [], []
    for value in values:
        if value not in positions:
            positions[value] = len(dictionary)
            dictionary.append(value)
        codes.append(positions[value])
    return dictionary, codes

symbols = ["AAPL", "MSFT", "AAPL", "AAPL", "MSFT", "NVDA"] * 1000
dictionary, codes = dictionary_encode(symbols)
print(dictionary, codes[:6])                         # ['AAPL', 'MSFT', 'NVDA'] [0, 1, 0, 0, 1, 2]
print([dictionary[c] for c in codes] == symbols)     # True  decoding restores the column
print(len(dictionary), "distinct values for", len(symbols), "rows")      # 3 distinct values for 6000 rows
# Statistics let a reader skip whole row groups without opening them
def groups_to_read(stats, low, high):
    return [i for i, (minimum, maximum) in enumerate(stats) if maximum >= low and minimum <= high]

stats = [(180.0, 183.5), (183.0, 186.0), (185.5, 190.0), (189.0, 195.0)]        # (min, max) of price per row group
print(groups_to_read(stats, 184.0, 185.0))           # [1]        only the second group can hold a price in this range
print(groups_to_read(stats, 189.5, 192.0))           # [2, 3]
print(groups_to_read(stats, 200.0, 210.0))           # []         no group needs to be read at all

Arrow、pandas、読む量を減らす

Apache Arrow はメモリ上で同じ列指向レイアウトを定義するので、プログラムや言語どうしが変換なしでテーブルを受け渡せます。pandas、Polars、DuckDB、Spark は、1つの Arrow テーブルをコピーなしで見られます。実際には pandas.read_parquet(path, columns=["symbol", "price"], filters=[("price", ">", 180)]) を使い、列の選択とフィルタをリーダーに押し込みます。pyarrow.parquet.read_table も使えます。日付のような列でパーティション分割して書き出せば(df.to_parquet(path, partition_cols=["date"]))、1日分のクエリは1つのディレクトリだけを開きます。行グループはよく圧縮できる大きさで、かつ統計で多くをスキップできる小ささにし、絞り込みに使う列で並べ替えて、各グループが狭い範囲をカバーするようにします。

# Sorting by the filter column makes the row groups cover narrow, non-overlapping ranges
import random

rng = random.Random(7)
prices = [round(rng.uniform(100, 200), 2) for _ in range(10_000)]

def group_stats(values, size):
    groups = [values[i : i + size] for i in range(0, len(values), size)]
    return [(min(g), max(g)) for g in groups]

def groups_to_read(stats, low, high):
    return [i for i, (a, b) in enumerate(stats) if b >= low and a <= high]

unsorted_groups = len(groups_to_read(group_stats(prices, 1000), 150.0, 151.0))
sorted_groups = len(groups_to_read(group_stats(sorted(prices), 1000), 150.0, 151.0))
print(unsorted_groups, sorted_groups)                # 10 1   without sorting every group's range overlaps the query
列指向ファイルは分析用で、編集用ではないParquet ファイルは一度書き、何度も読みます。1行を変えるだけでもファイルの書き直しになるため、履歴(ティック、バー、バックテスト結果)に向き、ライブの注文テーブルには向きません(それはデータベースの仕事です)。新しいデータは古いファイルを編集せず、新しいファイルやパーティションとして追加します。スキーマを安定させ、タイムスタンプは UTC にし、浮動小数の列には以前学んだ丸めがそのまま残ることを忘れないでください。
  • 列指向ストレージは各列をまとめて保持する。クエリは必要な列だけを読み、似た値はよく圧縮され、列の算術は速い
  • Parquet はデータを最小/最大の統計付きの行グループに分け、繰り返しの値を辞書でエンコードする。リーダーは不要なグループや列を読み飛ばす
  • Arrow は pandas・Polars・DuckDB が共有するメモリ上の双子。日付でパーティション分割し、絞り込み列で並べ替え、編集せず追記する

次は時系列データベースです。ティックやメトリクスのようなタイムスタンプ付きデータ向けのストアです。

ラボ:採点される内容

  • to_columns で行を列に変換する25点
  • to_rows で行を復元する25点
  • dictionary_encode で繰り返しを圧縮する25点
  • groups_to_read で読み飛ばせる部分を飛ばす25点
エクスプローラー · lab-128-1
▾ lab-128-1
…
bash · ブラウザ内
learner@quantcraft:~/lab-128-1$
Pythonは未読み込みPythonLn 1, Col 1Spaces: 4UTF-8

このトピックをリセットしますか?

lab-128-1 の内容はすべてスターターファイルに戻り、ターミナルも消去されます。現在の解答は失われ、元に戻せません。

リセットされるもの
  • main.py → スターターコード
  • ターミナル履歴とテスト出力
  • 未提出の変更
保持されるもの
  • ラボの最高スコア
  • XPと連続日数
  • 講義の進捗とメモ

コードを実行するにはログインしてください。

レクチャーはすべて無料で読めます。コードの実行、テスト、ラボの提出にはアカウントの作成かログインが必要です。

  • ブラウザ内IDEとbashターミナル。インストール不要
  • 即時採点、XP、ロードマップでの現在地
アカウントを作成ログイン
または
GitHubで続ける