MFormations
Modern Python Engineering

Chapitre 8

08-Data-Science

08-Data-Science

Data Science — Cours Complet

1. Pandas — Manipulation de Données

Structure de Base

import pandas as pd
import numpy as np

# Series — 1D
s = pd.Series([1, 2, 3, 4, 5], name="numbers")

# DataFrame — 2D
df = pd.DataFrame({
    "name": ["Alice", "Bob", "Charlie"],
    "age": [25, 30, 35],
    "salary": [50000, 60000, 70000],
})

# Lecture/Écriture
df_csv = pd.read_csv("data.csv")
df_parquet = pd.read_parquet("data.parquet")
df.to_csv("output.csv", index=False)
df.to_parquet("output.parquet")

Exploration

# Aperçu
df.head(10)
df.tail(5)
df.info()
df.describe()
df.shape
df.columns
df.dtypes

# Statistiques
df.describe(include="all")
df["age"].mean()
df["age"].median()
df["age"].std()
df["age"].quantile([0.25, 0.5, 0.75])

Selection et Filtrage

# Selection de colonnes
df["name"]
df[["name", "age"]]

# Selection de lignes
df.iloc[0]          # première ligne (index)
df.iloc[1:3]        # lignes 1-2
df.loc[0]           # par index label
df.loc[df["age"] > 30]  # condition

# Filtrage avancé
young_high_earners = df[
    (df["age"] < 30) & (df["salary"] > 55000)
]

# Query string
df.query("age > 25 and salary > 50000")

GroupBy

# GroupBy basique
df.groupby("category")["sales"].sum()

# Agrégations multiples
df.groupby("category").agg({
    "sales": ["sum", "mean", "count"],
    "profit": "sum",
})

# GroupBy avec transformations
df["sales_rank"] = df.groupby("region")["sales"].rank(ascending=False)

# GroupBy apply personnalisé
df.groupby("category").apply(
    lambda x: x.sort_values("sales", ascending=False).head(3)
)

Merge et Join

# Merge (SQL-style)
orders = pd.DataFrame({
    "order_id": [1, 2, 3],
    "customer_id": [101, 102, 101],
    "amount": [100, 200, 150],
})
customers = pd.DataFrame({
    "customer_id": [101, 102],
    "name": ["Alice", "Bob"],
})

# Inner join
result = orders.merge(customers, on="customer_id", how="inner")

# Left join
result = orders.merge(customers, on="customer_id", how="left")

# Concatenation
pd.concat([df1, df2], axis=0)  # rows
pd.concat([df1, df2], axis=1)  # columns

Pivot Tables

# Pivot
pivot = df.pivot_table(
    values="sales",
    index="region",
    columns="product",
    aggfunc="sum",
    fill_value=0,
)

# Melt (inverse de pivot)
melted = pd.melt(
    df,
    id_vars=["region"],
    value_vars=["product_a", "product_b"],
    var_name="product",
    value_name="sales",
)

Apply et Vectorization

# apply — mais préférer la vectorization
df["name_length"] = df["name"].apply(len)

# Vectorization (100-1000x plus rapide)
df["salary_raised"] = df["salary"] * 1.1
df["is_adult"] = df["age"] >= 18
df["full_name"] = df["first_name"] + " " + df["last_name"]

# map
df["gender_code"] = df["gender"].map({"Male": 0, "Female": 1})

# apply avec axis=1 (lent — à éviter sur grands datasets)
df["score"] = df.apply(
    lambda row: row["math"] + row["science"], axis=1
)

2. NumPy — Calcul Numérique

Arrays

import numpy as np

# Création
arr = np.array([1, 2, 3, 4, 5])
zeros = np.zeros((3, 4))
ones = np.ones((2, 3))
eye = np.eye(5)  # identité
random = np.random.randn(1000)

# Propriétés
arr.shape
arr.dtype
arr.ndim
arr.size
arr.nbytes  # mémoire

Broadcasting

# Broadcasting — opérations sur des shapes différents
a = np.array([[1, 2, 3], [4, 5, 6]])  # (2, 3)
b = np.array([10, 20, 30])            # (3,)
c = a + b                              # (2, 3) via broadcasting

# Règles du broadcasting
# 1. Aligner les dimensions par la droite
# 2. Les dimensions manquantes ou de taille 1 sont "broadcastées"

Algèbre Linéaire

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

# Multiplication matricielle
C = A @ B
C = np.matmul(A, B)
C = A.dot(B)

# Produit élément par élément
D = A * B

# Transposition
A.T

# Inverse
np.linalg.inv(A)

# Déterminant
np.linalg.det(A)

# Valeurs propres
eigvals, eigvecs = np.linalg.eig(A)

# SVD
U, S, Vt = np.linalg.svd(A)

3. Polars — DataFrames Haute Performance

Eager vs Lazy

import polars as pl

# Eager API (similaire Pandas)
df = pl.DataFrame({
    "name": ["Alice", "Bob", "Charlie"],
    "age": [25, 30, 35],
    "salary": [50000, 60000, 70000],
})

# Lazy API — optimisé
q = (
    pl.scan_csv("large_file.csv")
    .filter(pl.col("age") > 25)
    .group_by("department")
    .agg(pl.col("salary").mean())
    .sort("salary", descending=True)
)
df_result = q.collect()  # exécution paresseuse

Expression API

# API Expression (différente de Pandas)
df = pl.DataFrame({
    "name": ["Alice", "Bob", "Charlie"],
    "age": [25, 30, 35],
    "salary": [50000, 60000, 70000],
})

df.with_columns(
    (pl.col("salary") * 1.1).alias("salary_raised"),
    pl.when(pl.col("age") >= 30)
        .then(pl.lit("senior"))
        .otherwise(pl.lit("junior"))
        .alias("level"),
)

df.group_by("level").agg([
    pl.col("salary").mean().alias("avg_salary"),
    pl.col("age").count().alias("count"),
])

4. Visualisation

Matplotlib

import matplotlib.pyplot as plt

# Style
plt.style.use("seaborn-v0_8")

# Line plot
plt.figure(figsize=(10, 6))
plt.plot(x, y, label="Series A")
plt.plot(x, z, label="Series B")
plt.xlabel("X axis")
plt.ylabel("Y axis")
plt.title("Title")
plt.legend()
plt.grid(True)
plt.show()

# Subplots
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
axes[0, 0].plot(x, y)
axes[0, 1].scatter(x, y)
axes[1, 0].hist(y, bins=30)
axes[1, 1].boxplot([y, z])

Seaborn

import seaborn as sns

# Distribution
sns.histplot(data=df, x="age", hue="gender")
sns.kdeplot(data=df, x="salary", fill=True)

# Relations
sns.scatterplot(data=df, x="age", y="salary", hue="department")
sns.pairplot(df, hue="category")
sns.heatmap(df.corr(), annot=True, cmap="coolwarm")

# Catégories
sns.boxplot(data=df, x="department", y="salary")
sns.violinplot(data=df, x="department", y="salary")
sns.barplot(data=df, x="department", y="salary")

Plotly — Interactif

import plotly.express as px
import plotly.graph_objects as go

# Express (haut niveau)
fig = px.scatter(df, x="age", y="salary", color="department",
                 size="experience", hover_data=["name"])
fig.show()

# Graph Objects (bas niveau)
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=y, mode="lines+markers", name="Series"))
fig.update_layout(title="Interactive Plot", xaxis_title="X", yaxis_title="Y")
fig.show()

5. Jupyter Écosystème

Jupyter Lab

pip install jupyterlab voila
jupyter lab

Widgets interactifs

import ipywidgets as widgets
from IPython.display import display

@widgets.interact(x=(0, 10, 0.1), y=(0, 10, 0.1))
def plot_surface(x=5.0, y=5.0):
    plt.figure(figsize=(8, 6))
    plt.scatter([x], [y], s=200, c="red")
    plt.xlim(0, 10)
    plt.ylim(0, 10)
    plt.grid(True)
    plt.show()

Voila — Dashboard

voila notebook.ipynb  # transforme en application web

6. Performance — Vectorization vs Loops

import time

size = 10_000_000
data = np.random.randn(size)

# Loop Python (lent)
start = time.perf_counter()
result = []
for x in data:
    result.append(x * 2 + 1)
print(f"Loop: {time.perf_counter() - start:.2f}s")

# List comprehension
start = time.perf_counter()
result = [x * 2 + 1 for x in data]
print(f"List comp: {time.perf_counter() - start:.2f}s")

# NumPy vectorization (rapide)
start = time.perf_counter()
result = data * 2 + 1
print(f"Vectorized: {time.perf_counter() - start:.2f}s")

7. ETL Pipelines

"""Pipeline ETL typique."""

from typing import Generator
import pandas as pd
import logging

logger = logging.getLogger(__name__)

def extract(filepath: str, chunk_size: int = 10000) -> Generator[pd.DataFrame, None, None]:
    """Extract — lecture par chunks."""
    for chunk in pd.read_csv(filepath, chunksize=chunk_size):
        logger.info(f"Extracted {len(chunk)} rows")
        yield chunk

def transform(df: pd.DataFrame) -> pd.DataFrame:
    """Transform — nettoyage et enrichissement."""
    df = df.dropna(subset=["id", "value"])
    df["value"] = pd.to_numeric(df["value"], errors="coerce")
    df["date"] = pd.to_datetime(df["date"])
    df["year_month"] = df["date"].dt.to_period("M")
    return df

def load(df: pd.DataFrame, output_path: str, mode: str = "a") -> None:
    """Load — écriture parquet."""
    df.to_parquet(
        output_path,
        engine="pyarrow",
        compression="snappy",
        append=(mode == "a"),
    )

def run_pipeline(input_path: str, output_path: str) -> None:
    """Exécute le pipeline ETL complet."""
    for i, chunk in enumerate(extract(input_path)):
        transformed = transform(chunk)
        mode = "w" if i == 0 else "a"
        load(transformed, output_path, mode)
        logger.info(f"Processed chunk {i}: {len(transformed)} rows")