σμ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 AcademyRoteiros
Ctrl K
AO
+1,2 mil alunos
EntrarParticipar ↗
Roteiros
AO
+1,2 mil alunos
Entrar
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Roteiro de Python›Módulo 10 · Engenharia Python de alto desempenho›Tópico 10.12·~50 minAvançado

Parquet e Arrow

Posição no roteiroMódulo 10 de 11 · Tópico 12 de 13
10.12 Parquet e Arrow
—Nota do lab— / 100XP total—Sequência—

Linhas ou colunas?

Um arquivo CSV ou um banco orientado a linhas guarda cada registro junto: todos os campos do negócio 1, depois do 2, e assim por diante. A análise raramente quer isso. Calcular o preço médio de dez milhões de negócios só precisa da coluna price, mas um layout por linhas obriga o leitor a pular todos os outros campos. Um layout colunar mantém cada coluna contígua: a consulta lê só as colunas que nomeia, valores parecidos ficam lado a lado e comprimem muito bem, e a aritmética sobre uma coluna percorre um único bloco compacto de memória. Por isso os data lakes quantitativos usam Parquet em disco e Arrow na memória.

# 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: grupos de linhas, estatísticas e codificações

Um arquivo Parquet divide a tabela em grupos de linhas (digamos um milhão de linhas cada); dentro de um grupo cada coluna é guardada como um pedaço próprio, e o arquivo registra o mínimo e o máximo de cada pedaço. Uma consulta de price entre 180 e 190 lê primeiro as estatísticas e pula todo grupo cujo intervalo não pode conter tal preço (predicate pushdown), e lê só as colunas pedidas (projection pushdown). As colunas também são codificadas: uma coluna com poucos valores distintos, como o símbolo, é guardada como um pequeno dicionário mais um minúsculo código inteiro por linha, e o resultado é comprimido. Um arquivo de dados de mercado repetitivos costuma ser dez vezes menor que o CSV.

# 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 e ler menos

O Apache Arrow define o mesmo layout colunar para a memória, de modo que programas e linguagens podem passar uma tabela entre si sem convertê-la: pandas, Polars, DuckDB e Spark podem ver uma mesma tabela Arrow sem cópia. Na prática usa-se pandas.read_parquet(path, columns=["symbol", "price"], filters=[("price", ">", 180)]), que empurra a escolha de colunas e o filtro para o leitor, ou pyarrow.parquet.read_table. Escreva os dados particionados por uma coluna como a data (df.to_parquet(path, partition_cols=["date"])) para que uma consulta de um dia abra um só diretório. Mantenha grupos de linhas grandes o bastante para comprimir bem mas pequenos o bastante para que as estatísticas descartem muitos, e ordene os dados pela coluna de filtro para que cada grupo cubra um intervalo estreito.

# 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
Um arquivo colunar é para análise, não para ediçãoArquivos Parquet são escritos uma vez e lidos muitas. Mudar uma linha obriga a reescrever um arquivo, então servem para histórico (ticks, barras, resultados de backtest) e não para uma tabela de ordens ao vivo, que pertence a um banco de dados. Acrescente dados novos como arquivos ou partições novos em vez de editar os antigos. Mantenha o esquema estável e os carimbos de tempo em UTC, e lembre que colunas de float mantêm o arredondamento que você viu antes.
  • O armazenamento colunar mantém cada coluna junta: consultas leem só as colunas necessárias, valores parecidos comprimem bem e a aritmética por coluna é rápida.
  • O Parquet divide os dados em grupos de linhas com estatísticas mín/máx e codifica por dicionário os valores repetidos; os leitores pulam grupos e colunas desnecessários.
  • O Arrow é o gêmeo em memória compartilhado por pandas, Polars e DuckDB; particione por data, ordene pela coluna de filtro, acrescente em vez de editar.

A seguir: bancos de dados de séries temporais, armazenamentos feitos para dados com carimbo de tempo, como ticks e métricas.

Lab: o que é avaliado

  • Transformar linhas em colunas com to_columns25 pts
  • Reconstruir as linhas com to_rows25 pts
  • Reduzir repetições com dictionary_encode25 pts
  • Pular o que for possível com groups_to_read25 pts
Explorador · lab-128-1
▾ lab-128-1
…
bash · no navegador
learner@quantcraft:~/lab-128-1$
Python ainda não carregadoPythonLn 1, Col 1Spaces: 4UTF-8

Redefinir este tópico?

Tudo em lab-128-1 volta aos arquivos iniciais e o terminal é limpo. Sua solução atual será perdida — isso não pode ser desfeito.

Será redefinido
  • main.py → código inicial
  • Histórico do terminal e saída dos testes
  • Alterações não enviadas
Será mantido
  • Melhor nota do lab
  • Seu XP e sua sequência
  • Progresso da aula e anotações

Entre para executar o seu código.

Todas as aulas são gratuitas para ler. Crie uma conta ou entre para executar código, rodar os testes e enviar este lab.

  • IDE no navegador com terminal bash — nada para instalar
  • Correção instantânea, XP e o seu lugar no roteiro
Criar uma contaEntrar
OU
Continuar com o GitHub