σμ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 AcademyParcours
Ctrl K
AO
+1,2 k apprenants
ConnexionRejoindre ↗
Parcours
AO
+1,2 k apprenants
Connexion
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Parcours Python›Module 10 · Ingénierie Python haute performance›Sujet 10.12·~50 minAvancé

Parquet et Arrow

Position dans le parcoursModule 10 sur 11 · Sujet 12 sur 13
10.12 Parquet et Arrow
—Score du lab— / 100XP total—Série—

Lignes ou colonnes ?

Un fichier CSV ou une base orientée lignes garde chaque enregistrement ensemble : tous les champs de la transaction 1, puis de la transaction 2, etc. L'analyse le veut rarement. Calculer le prix moyen de dix millions de transactions n'a besoin que de la colonne price, mais une disposition par lignes oblige le lecteur à enjamber tous les autres champs. Une disposition en colonnes garde chaque colonne contiguë : une requête ne lit que les colonnes qu'elle nomme, les valeurs semblables sont côte à côte et se compressent très bien, et l'arithmétique sur une colonne parcourt un seul bloc compact de mémoire. C'est pourquoi les lacs de données quantitatifs utilisent Parquet sur disque et Arrow en mémoire.

# 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 : groupes de lignes, statistiques et encodages

Un fichier Parquet découpe la table en groupes de lignes (disons un million de lignes chacun) ; dans un groupe, chaque colonne est stockée comme un morceau à part, et le fichier enregistre le minimum et le maximum de chaque morceau. Une requête sur price entre 180 et 190 lit d'abord les statistiques et ignore tout groupe dont l'intervalle ne peut pas contenir un tel prix (pushdown de prédicat), et ne lit que les colonnes demandées (pushdown de projection). Les colonnes sont aussi encodées : une colonne à peu de valeurs distinctes, comme le symbole, est stockée comme un petit dictionnaire plus un minuscule code entier par ligne, puis le résultat est compressé. Un fichier de données de marché répétitives est souvent dix fois plus petit que le 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 et lire moins

Apache Arrow définit la même disposition en colonnes pour la mémoire, si bien que programmes et langages peuvent se passer une table sans la convertir : pandas, Polars, DuckDB et Spark peuvent tous voir une même table Arrow sans copie. En pratique on utilise pandas.read_parquet(path, columns=["symbol", "price"], filters=[("price", ">", 180)]), qui pousse le choix des colonnes et le filtre dans le lecteur, ou pyarrow.parquet.read_table. Écrivez les données partitionnées par une colonne comme la date (df.to_parquet(path, partition_cols=["date"])) pour qu'une requête sur un jour n'ouvre qu'un répertoire. Gardez des groupes de lignes assez grands pour bien compresser mais assez petits pour que les statistiques en éliminent beaucoup, et triez les données selon la colonne de filtre pour que chaque groupe couvre un intervalle étroit.

# 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
Un fichier en colonnes sert à l'analyse, pas aux modificationsLes fichiers Parquet s'écrivent une fois et se lisent beaucoup. Modifier une ligne oblige à réécrire un fichier : ils conviennent à l'historique (ticks, barres, résultats de backtest) et non à une table d'ordres en direct, qui relève d'une base de données. Ajoutez les nouvelles données comme nouveaux fichiers ou partitions plutôt que de modifier les anciens. Gardez un schéma stable et des horodatages en UTC, et rappelez-vous que les colonnes de flottants gardent l'arrondi vu plus tôt.
  • Le stockage en colonnes garde chaque colonne ensemble : les requêtes ne lisent que les colonnes utiles, les valeurs semblables se compressent bien et l'arithmétique par colonne est rapide.
  • Parquet découpe les données en groupes de lignes avec statistiques min/max et encode par dictionnaire les valeurs répétées ; les lecteurs ignorent les groupes et colonnes inutiles.
  • Arrow est le jumeau en mémoire partagé par pandas, Polars et DuckDB ; partitionnez par date, triez selon la colonne de filtre, ajoutez au lieu de modifier.

À suivre : les bases de données de séries temporelles, des stockages conçus pour les données horodatées comme les ticks et les métriques.

Lab : ce qui est noté

  • Transformer des lignes en colonnes avec to_columns25 pts
  • Reconstruire les lignes avec to_rows25 pts
  • Réduire les répétitions avec dictionary_encode25 pts
  • Ignorer ce qui peut l'être avec groups_to_read25 pts
Explorateur · lab-128-1
▾ lab-128-1
…
bash · dans le navigateur
learner@quantcraft:~/lab-128-1$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

Tout le contenu de lab-128-1 revient aux fichiers de départ et le terminal est vidé. Votre solution actuelle sera perdue : cette action est irréversible.

Sera réinitialisé
  • main.py → code de départ
  • Historique du terminal et sortie des tests
  • Modifications non soumises
Sera conservé
  • Meilleur score du lab
  • Votre XP et votre série
  • Progression du cours et notes

Connectez-vous pour exécuter votre code.

Tous les cours sont gratuits à lire. Créez un compte ou connectez-vous pour exécuter du code, lancer les tests et soumettre ce lab.

  • IDE dans le navigateur avec terminal bash — rien à installer
  • Correction instantanée, XP et votre place dans le parcours
Créer un compteSe connecter
OU
Continuer avec GitHub