# Python voor Data Science: NumPy, Pandas en Scikit-Learn in 2026 > Een praktijkgerichte handleiding voor Python data science met NumPy 2.1, Pandas 2.2 en Scikit-Learn 1.6. Van ruwe data tot een volledig getraind machine learning-model met productie-ready code. - Published: 2026-04-07 - Updated: 2026-04-07 - Author: Anthony Fillion-Maillet - Tags: data-science, python, numpy, pandas, scikit-learn, machine-learning, tutorial - Reading time: 10 min --- Python blijft in 2026 de onbetwiste standaard voor data science. Het ecosysteem rondom NumPy, Pandas en Scikit-Learn heeft zich de afgelopen jaren verder geconsolideerd en biedt ontwikkelaars en datawetenschappers een stabiele, performante basis voor het hele traject van ruwe data tot productiewaardige modellen. Deze handleiding doorloopt dat volledige traject stap voor stap: van array-bewerkingen en dataopschoning tot het trainen, valideren en exporteren van een machine learning-pipeline. Alle codevoorbeelden zijn direct uitvoerbaar met Python 3.12 en de nieuwste bibliotheekversies. > **Gebruikte versies in deze tutorial** > > De voorbeelden in dit artikel zijn geschreven en getest met **Python 3.12**, **NumPy 2.1**, **Pandas 2.2** en **Scikit-Learn 1.6**. Oudere versies kunnen afwijkend gedrag vertonen, met name rond Copy-on-Write in Pandas en de vernieuwde array-API in NumPy. ## NumPy Array-bewerkingen voor Efficiente Berekeningen NumPy vormt het rekenkundige fundament van vrijwel elke data science-stack in Python. Het kernprincipe is eenvoudig maar krachtig: bewerkingen worden niet element voor element in een Python-loop uitgevoerd, maar gevectoriseerd op C-niveau. Dat levert bij grote datasets snelheidswinsten op van factor 10 tot 100 ten opzichte van standaard Python-lijsten. ```python # numpy_basics.py import numpy as np # Create arrays from different sources prices = np.array([29.99, 49.99, 19.99, 99.99, 39.99]) quantities = np.arange(1, 6) # [1, 2, 3, 4, 5] # Vectorized arithmetic — no loops needed revenue = prices * quantities print(revenue) # [29.99, 99.98, 59.97, 399.96, 199.95] # Statistical aggregations print(f"Total revenue: ${revenue.sum():.2f}") # $789.85 print(f"Mean price: ${prices.mean():.2f}") # $47.99 print(f"Std deviation: ${prices.std():.2f}") # $27.64 # Boolean indexing — filter without loops premium_mask = prices > 40 premium_items = prices[premium_mask] # [49.99, 99.99] ``` Boolean indexing is een van de meest onderschatte features van NumPy. In plaats van een `for`-loop met een `if`-conditie maakt het een vergelijkingsoperatie op de gehele array in een keer, wat resulteert in een boolean-masker. Dat masker dient vervolgens als index om alleen de elementen te selecteren die aan de voorwaarde voldoen. Dit patroon komt overal terug in data science: bij het filteren van uitschieters, het selecteren van rijen op basis van drempelwaarden, of het toepassen van conditionele transformaties. Het is niet alleen sneller dan een loop, maar ook compacter en minder foutgevoelig. ## Reshaping en Broadcasting in NumPy Bij het werken met multidimensionale data is het essentieel om te begrijpen hoe NumPy de assen van een array interpreteert. De parameter `axis` bepaalt langs welke dimensie een operatie wordt uitgevoerd: `axis=0` werkt kolomsgewijs (over de rijen heen) en `axis=1` rijsgewijs (over de kolommen heen). Broadcasting maakt het bovendien mogelijk om arrays van verschillende vormen met elkaar te combineren zonder expliciete loops. ```python # numpy_reshape.py import numpy as np # Monthly sales data: 4 products x 3 months sales = np.array([ [120, 150, 130], # Product A [200, 180, 220], # Product B [90, 110, 95], # Product C [300, 280, 310], # Product D ]) # Column-wise mean (average per month) monthly_avg = sales.mean(axis=0) # [177.5, 180.0, 188.75] # Row-wise sum (total per product) product_totals = sales.sum(axis=1) # [400, 600, 295, 890] # Normalize each product relative to its own max normalized = sales / sales.max(axis=1, keepdims=True) # keepdims=True preserves the shape for broadcasting print(normalized[0]) # [0.8, 1.0, 0.867] — Product A relative to its peak # Reshape for Scikit-Learn (requires 2D input) flat_sales = sales.flatten() # 1D array of 12 values reshaped = flat_sales.reshape(-1, 1) # 12x1 column vector ``` De `axis`-parameter is een veelvoorkomende bron van verwarring bij beginners. Een handige ezelsbruggetje: de waarde van `axis` geeft aan welke dimensie *verdwijnt* na de operatie. Bij een matrix van 4x3 levert `axis=0` een resultaat van vorm (3,) op, omdat de rij-dimensie wordt samengevoegd. Omgekeerd geeft `axis=1` een resultaat van vorm (4,), omdat de kolom-dimensie verdwijnt. Het argument `keepdims=True` voorkomt dit vormverlies, wat onmisbaar is wanneer het resultaat later via broadcasting met de oorspronkelijke array moet worden gecombineerd, zoals bij normalisatie per rij. ## Pandas DataFrame-manipulatie en Dataopschoning In de praktijk begint elk data science-project met rommelige data. Kolommen bevatten ontbrekende waarden, datatypes kloppen niet, en er zitten duplicaten of onmogelijke waarden in de dataset. Pandas biedt een uitgebreide gereedschapskist om deze opschoningsstappen reproduceerbaar en transparant uit te voeren. Het gebruik van method chaining maakt het mogelijk om meerdere transformaties in een leesbare keten te schrijven, zonder tussentijdse variabelen die de namespace vervuilen. ```python # pandas_cleaning.py import pandas as pd import numpy as np # Load and inspect raw data df = pd.read_csv("candidates.csv") print(df.shape) # (1500, 8) print(df.dtypes) # Check column types print(df.isna().sum()) # Count missing values per column # Clean in a reproducible chain df_clean = ( df .dropna(subset=["salary", "experience_years"]) # Drop rows missing critical fields .assign( salary=lambda x: x["salary"].clip(lower=20000, upper=500000), # Cap outliers experience_years=lambda x: x["experience_years"].astype(int), hired_date=lambda x: pd.to_datetime(x["hired_date"], errors="coerce"), ) .drop_duplicates(subset=["email"]) # Remove duplicate candidates .query("experience_years >= 0") # Filter invalid entries .reset_index(drop=True) ) print(f"Cleaned: {len(df)} -> {len(df_clean)} rows") ``` Method chaining is meer dan een stilistische voorkeur — het dwingt een functionele, stapsgewijze denkwijze af die het opschoningsproces documenteert in de code zelf. Elke stap in de keten ontvangt het DataFrame van de vorige stap en retourneert een nieuw (of gewijzigd) DataFrame. De `.assign()`-methode is bijzonder krachtig omdat lambda-functies toegang hebben tot het DataFrame zoals het er op dat punt in de keten uitziet, niet zoals het er oorspronkelijk uitzag. Dit voorkomt subtiele bugs die ontstaan wanneer transformaties van elkaar afhankelijk zijn. Voor productieomgevingen verdient het aanbeveling om de keten te voorzien van commentaar per stap, zodat collega's de redenering achter elke transformatie direct kunnen volgen. > **Pandas 2.2 Copy-on-Write** > > Vanaf Pandas 2.2 is Copy-on-Write (CoW) het standaardgedrag. Dit betekent dat elke indexeringsoperatie een **lazy copy** retourneert: de data wordt pas echt gekopieerd op het moment dat een van de twee objecten wordt gewijzigd. In de praktijk elimineert dit de beruchte `SettingWithCopyWarning` volledig en voorkomt het onbedoelde mutaties van het oorspronkelijke DataFrame. Code die eerder expliciet `.copy()` gebruikte, werkt nog steeds correct maar is niet langer noodzakelijk. Het is wel raadzaam om bestaande code te testen met `pd.options.mode.copy_on_write = True` voordat naar Pandas 2.2 wordt geupgraded. ## GroupBy-aggregaties en Feature Engineering met Pandas Nadat de data is opgeschoond, is de volgende stap het extraheren van betekenisvolle patronen en het construeren van features die een machine learning-model kan gebruiken. De `groupby()`-methode van Pandas is hiervoor het centrale werkpaard: het groepeert rijen op basis van een of meerdere kolommen en past vervolgens een aggregatiefunctie toe per groep. Voor [feature engineering in de context van data science-sollicitaties](/technologies/data-science/interview-questions/feature-engineering) is het cruciaal om zowel geaggregeerde statistieken als relatieve maatstaven te beheersen. ```python # pandas_groupby.py import pandas as pd # Aggregate candidate stats by department dept_stats = ( df_clean .groupby("department") .agg( avg_salary=("salary", "mean"), median_experience=("experience_years", "median"), headcount=("email", "count"), max_salary=("salary", "max"), ) .sort_values("avg_salary", ascending=False) ) print(dept_stats.head()) # Create features for ML: encode categorical + add aggregated stats df_features = ( df_clean .assign( # Ratio of individual salary to department average salary_ratio=lambda x: x["salary"] / x.groupby("department")["salary"].transform("mean"), # Time since hire in days tenure_days=lambda x: (pd.Timestamp.now() - x["hired_date"]).dt.days, # Binary encoding is_senior=lambda x: (x["experience_years"] >= 5).astype(int), ) ) ``` De `.transform()`-methode verdient bijzondere aandacht. Waar `.agg()` het aantal rijen reduceert tot een per groep, behoudt `.transform()` de oorspronkelijke index en voegt het geaggregeerde resultaat terug aan elke rij van de groep. Dit is precies wat nodig is bij feature engineering: het salaris van een individuele kandidaat delen door het gemiddelde van diens afdeling vereist dat het afdelingsgemiddelde beschikbaar is op rij-niveau. Zonder `.transform()` zou dit een aparte `groupby()` plus een `merge()` vergen, wat zowel trager als minder leesbaar is. Het patroon `x.groupby(...)[kolom].transform(functie)` binnen `.assign()` is daarmee een van de meest waardevolle idiomen in Pandas voor data science-toepassingen. ## Een Scikit-Learn Pipeline vanaf Nul Opbouwen Met opgeschoonde data en geengineerde features is het tijd om het machine learning-model op te zetten. Scikit-Learn biedt met de `Pipeline`- en `ColumnTransformer`-klassen een elegant mechanisme om preprocessing en modellering samen te voegen in een enkel object. Dit is niet alleen handig — het is essentieel om data leakage te voorkomen en om het gehele model reproduceerbaar op te slaan. ```python # sklearn_pipeline.py from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.ensemble import GradientBoostingClassifier from sklearn.metrics import classification_report import pandas as pd # Define column groups numeric_features = ["salary", "experience_years", "salary_ratio", "tenure_days"] categorical_features = ["department", "role_level"] target = "promoted" # Split before any preprocessing X = df_features[numeric_features + categorical_features] y = df_features[target] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # Build the preprocessing + model pipeline preprocessor = ColumnTransformer( transformers=[ ("num", StandardScaler(), numeric_features), # Scale numeric columns ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features), # Encode categories ] ) pipeline = Pipeline([ ("preprocessor", preprocessor), ("classifier", GradientBoostingClassifier( n_estimators=200, learning_rate=0.1, max_depth=4, random_state=42, )), ]) # Train and evaluate pipeline.fit(X_train, y_train) y_pred = pipeline.predict(X_test) print(classification_report(y_test, y_pred)) ``` De `ColumnTransformer` is het scharnierpunt van de pipeline. In vrijwel elke dataset bestaan numerieke en categorische kolommen naast elkaar, en elke groep vereist een andere voorbewerking. De `ColumnTransformer` past de juiste transformer toe op de juiste subset van kolommen en voegt de resultaten horizontaal samen tot een enkele featurematrix. Het argument `handle_unknown="ignore"` bij `OneHotEncoder` zorgt ervoor dat categoriewaarden die wel in de testset maar niet in de trainingsset voorkomen, worden genegeerd in plaats van een fout te veroorzaken. Dit is een kleine maar cruciale instelling voor productietoepassingen waar de invoerdata niet altijd voorspelbaar is. ## Kruisvalidatie en Hyperparameter-optimalisatie Een model trainen op een enkele train/test-split geeft een beperkt beeld van de werkelijke prestaties. Kruisvalidatie verdeelt de trainingsdata in meerdere folds en traint het model herhaaldelijk, telkens met een andere fold als validatieset. Dit levert een robuustere schatting van de modelprestaties op en legt eventuele overfitting bloot die bij een enkele split verborgen zou blijven. ```python # sklearn_tuning.py from sklearn.model_selection import cross_val_score, GridSearchCV import numpy as np # 5-fold cross-validation on the full pipeline scores = cross_val_score(pipeline, X_train, y_train, cv=5, scoring="f1") print(f"F1 scores: {scores}") print(f"Mean F1: {scores.mean():.3f} (+/- {scores.std() * 2:.3f})") # Grid search over hyperparameters param_grid = { "classifier__n_estimators": [100, 200, 300], "classifier__max_depth": [3, 4, 5], "classifier__learning_rate": [0.05, 0.1, 0.2], } grid_search = GridSearchCV( pipeline, param_grid, cv=5, scoring="f1", n_jobs=-1, # Use all CPU cores verbose=1, ) grid_search.fit(X_train, y_train) print(f"Best params: {grid_search.best_params_}") print(f"Best F1: {grid_search.best_score_:.3f}") # Evaluate the best model on held-out test set best_model = grid_search.best_estimator_ print(classification_report(y_test, best_model.predict(X_test))) ``` De dubbele-underscore-syntax (`classifier__n_estimators`) is Scikit-Learns manier om parameters van geneste objecten binnen een pipeline aan te spreken. Het deel voor de dubbele underscore verwijst naar de naam van de stap in de pipeline (in dit geval `"classifier"`), en het deel erna naar de parameter van dat specifieke object. Dit mechanisme werkt recursief: bij een geneste `ColumnTransformer` binnen een pipeline kan een parameternaam er uitzien als `preprocessor__num__with_mean`. `GridSearchCV` combineert deze syntax met kruisvalidatie om systematisch elke combinatie van hyperparameters te evalueren. Met `n_jobs=-1` worden alle beschikbare CPU-kernen ingezet, wat bij 27 combinaties en 5 folds het verschil kan zijn tussen minuten en uren rekentijd. > **Pas op voor data leakage** > > Voer preprocessing **altijd** uit binnen de pipeline, niet ervoor. Wanneer scaling of encoding wordt toegepast op de volledige dataset voordat de train/test-split plaatsvindt, lekt informatie uit de testset naar het trainingsproces. De pipeline garandeert dat elke fold in de kruisvalidatie onafhankelijk wordt getransformeerd op basis van uitsluitend de trainingsdata van die fold. Dit geldt ook voor feature engineering-stappen zoals het berekenen van gemiddelden per groep. ## Modellen Opslaan en Laden voor Productie Nadat het beste model is geselecteerd via kruisvalidatie en hyperparameter-tuning, moet het worden opgeslagen zodat het in een productieomgeving kan worden ingezet zonder het trainingsproces te herhalen. De `joblib`-bibliotheek serialiseert het complete pipeline-object, inclusief de preprocessing-stappen en het getrainde model, naar een enkel bestand. ```python # sklearn_export.py import joblib from pathlib import Path # Save the complete pipeline (preprocessor + model) model_dir = Path("models") model_dir.mkdir(exist_ok=True) joblib.dump(best_model, model_dir / "promotion_model_v1.joblib") # Load and predict in a different process loaded_model = joblib.load(model_dir / "promotion_model_v1.joblib") new_data = pd.DataFrame({ "salary": [75000], "experience_years": [4], "salary_ratio": [1.05], "tenure_days": [730], "department": ["Engineering"], "role_level": ["Mid"], }) prediction = loaded_model.predict(new_data) probability = loaded_model.predict_proba(new_data)[:, 1] print(f"Promoted: {bool(prediction[0])}, Confidence: {probability[0]:.2%}") ``` Het opslaan van de volledige pipeline in plaats van alleen het model is een bewuste keuze. Wanneer uitsluitend het getrainde model wordt geexporteerd, moet de preprocessing-logica apart worden gerepliceerd in de productieomgeving, wat een veelvoorkomende bron van fouten is. Door de `Pipeline` als geheel te serialiseren, accepteert `loaded_model.predict()` dezelfde ruwe invoerdata als tijdens het trainen: de `StandardScaler` en `OneHotEncoder` worden automatisch toegepast met de parameters die tijdens het fitten zijn geleerd. Bij het versiebeheer van modellen is het raadzaam om zowel het `.joblib`-bestand als de bijbehorende Python-omgevingspecificaties (via `requirements.txt` of `pyproject.toml`) op te slaan, zodat compatibiliteitsproblemen bij het laden kunnen worden voorkomen. ## Conclusie Deze handleiding heeft het volledige traject doorlopen van ruwe data tot een productiewaardige machine learning-pipeline. De kernpunten samengevat: - **NumPy** biedt gevectoriseerde bewerkingen die Python-loops overbodig maken. Boolean indexing, broadcasting en de `axis`-parameter vormen de basis voor efficiente numerieke berekeningen. - **Pandas** transformeert rommelige datasets via method chaining en `.assign()` tot schone, gestructureerde DataFrames. De `groupby()`/`.transform()`-combinatie is onmisbaar voor feature engineering op rij-niveau. - **Scikit-Learn Pipelines** bundelen preprocessing en modellering in een enkel object, waardoor data leakage wordt voorkomen en het model direct inzetbaar is in productie. - **Kruisvalidatie** met `GridSearchCV` geeft een betrouwbare schatting van modelprestaties en optimaliseert hyperparameters systematisch. - **Serialisatie** via `joblib` slaat de complete pipeline op, zodat preprocessing en model altijd synchroon blijven. De combinatie van deze drie bibliotheken dekt het overgrote deel van de data science-werkzaamheden in de praktijk. Voor verdere verdieping is het aan te raden om te experimenteren met alternatieve modellen binnen dezelfde pipeline-structuur — de modulaire opzet van Scikit-Learn maakt het verwisselen van een `GradientBoostingClassifier` voor een `RandomForestClassifier` of `LogisticRegression` tot een eenregelige aanpassing. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/data-science/python-data-science-numpy-pandas-scikit-learn-2026