Data Scientist & ML Engineer — building data-driven solutions from gene ontology graphs and physiological signals to large-scale LLM inference and network latency research.
A curated selection of projects spanning bioinformatics, signal processing, cloud AI infrastructure, and network data analysis — showcasing diverse data-driven problem solving.
Research Assistant Position — Bioinformatics
Given a large file of gene–GO (Gene Ontology) annotations and a GO class hierarchy, parse and link genes to their GO terms. Then propagate annotations upward through the hierarchy — if a gene is annotated to class X, it's also annotated to all parent classes of X. Finally, rank the top 50 GO classes by how many genes they cover.
GO:XXXXXXX format# Parse annotations: build gene → GO and GO → gene mappings go_classes_per_gene = defaultdict(set) genes_per_go_class = defaultdict(set) with open(annotations_file) as f: for line in f: if line.startswith("!"): continue cols = line.strip().split("\t") gene_id = cols[1] go_class = cols[4] go_classes_per_gene[gene_id].add(go_class) genes_per_go_class[go_class].add(gene_id)
# Propagate annotations to parent GO classes # "If a gene is annotated to class X, it is also # annotated to all parent classes of X" if go_class in genes_per_go_class.keys(): for parent_id in parents.split(","): parent_id = parent_id.strip() if not parent_id.startswith("GO:"): parent_id = f"GO:{parent_id.zfill(7)}" for gene_id in genes_per_go_class[go_class]: go_classes_per_gene[gene_id].add(parent_id)
# Recount and rank the top 50 GO classes by gene count for gene_id, go_class_set in go_classes_per_gene.items(): for go_class in go_class_set: genes_per_go_class[go_class].add(gene_id) top_50 = sorted( genes_per_go_class.items(), key=lambda x: len(x[1]), reverse=True )[:50] for go_id, genes in top_50: name = go_class_names.get(go_id, "Unknown") print(f"{go_id:<15} {name:<50} {len(genes)}")
Physiological Signal Analysis — Sports Science
Analyze a runner's exercise data — R-R intervals (RRI, heartbeat timing) and GPS speed — to detect the lactate threshold: the exercise intensity where lactic acid starts accumulating faster than the body can clear it. This is a key metric in sports science for training optimization.
# RMSSD: Root Mean Square of Successive Differences # A measure of heart rate variability (HRV) def calculate_rmssd(rri_window): if len(rri_window) < 2: return 0 diff = np.diff(rri_window) return np.sqrt(np.mean(diff ** 2)) # Clean outliers: replace physiologically impossible values # <300ms = tachycardia/artifact, >3000ms = sinus arrest rri_values[(rri_values >= 3000) | (rri_values <= 300)] = \ np.median(rri_values)
@dataclass class LactateThresholdState: """State for lactate threshold detection""" min_trend_length: int = 10 # Previous values prev_rmssd: Optional[float] = None prev_index: int = 0 # Decreasing trend tracking decrease_trend_count: int = 0 decrease_trend_confirmed: bool = False # Increasing trend tracking increase_trend_count: int = 0 increase_start_rmssd: Optional[float] = None increase_start_speed: Optional[float] = None
# Two-phase detection: first find a sustained decrease # in RMSSD, then find the reversal (increase) # Phase 1: Decreasing trend if not state.decrease_trend_confirmed: if prev_val > current_val: state.decrease_trend_count += 1 elif count >= state.min_trend_length: state.decrease_trend_confirmed = True # Phase 2: Increasing trend (reversal found!) else: if prev_val < current_val: state.increase_trend_count += 1 elif count >= state.min_trend_length: return Status.THRESHOLD_FOUND, state
AI Infrastructure — Cloud GPU Computing
Set up and run inference on large-scale language models (70B+ parameters) using CSC's LUMI supercomputer — one of Europe's most powerful HPC systems. The goal was to serve a Poro 70B Instruct model via vLLM for SRT subtitle translation tasks using multi-GPU tensor parallelism.
#!/bin/bash #SBATCH --job-name=prompting #SBATCH --partition=dev-g #SBATCH --gpus-per-node=8 #SBATCH --mem=480G #SBATCH --cpus-per-task=56 #SBATCH --time=01:00:00 module use /appl/local/csc/modulefiles/ module load pytorch/2.5 # Deepseek-R1-Distill-Llama-70B requires 132GB export HF_HOME=hf-cache
# Launch vLLM OpenAI-compatible API server # with tensor parallelism across 8 GPUs MODEL="LumiOpen/Llama-Poro-2-70B-Instruct" CHAT_TEMPLATE="poro_chat_template.jinja" python -m vllm.entrypoints.openai.api_server \ --model=$MODEL \ --tensor-parallel-size 8 \ --pipeline-parallel-size 1 \ --gpu-memory-utilization=0.95
# Send translation prompts via OpenAI API def get_translated_text(text, lang, url, model): prompt = system_prompt.format(lang=lang) messages = [ {"role": "system", "content": prompt}, {"role": "user", "content": text} ] headers = {"Content-Type": "application/json"} data = { "model": model, "messages": messages, "temperature": 0.7 } resp = requests.post(url, headers=headers, data=json.dumps(data)) return resp.json()
Data Analysis — Networking & Geospatial
Analyze live network latency data from RIPE Atlas — a global internet measurement network — to generate realistic scenarios for simulating edge computing environments. Using Amsterdam as the target city, map real probe/anchor devices to city districts and measure round-trip times (RTT) between end devices and edge servers.
# Filter RIPE Atlas anchors within Amsterdam districts # using GeoPandas spatial join import geopandas as gpd # Load Amsterdam district boundaries (WGS84 / EPSG:4326) districts = gpd.read_file("amsterdam_gebieden.geojson") anchors_gdf = gpd.GeoDataFrame( anchors, geometry=gpd.points_from_xy( anchors.longitude, anchors.latitude ), crs="EPSG:4326" ) # Spatial join: keep only anchors inside districts filtered = gpd.sjoin(anchors_gdf, districts, how="inner", predicate="within")
# Fetch RTT measurements from RIPE Atlas API # for selected anchor-probe pairs import requests def fetch_rtt_measurements( measurement_id, start, stop ): url = (f"https://atlas.ripe.net/api/v2/" f"measurements/{measurement_id}/" f"results/?start={start}&stop={stop}") response = requests.get(url) results = response.json() rtts = [] for result in results: for reply in result.get("result", []): if "rtt" in reply: rtts.append(reply["rtt"]) return rtts
# Compute summary RTT statistics per region import numpy as np from scipy import stats def compute_rtt_stats(rtt_values): return { "min": np.min(rtt_values), "max": np.max(rtt_values), "mean": np.mean(rtt_values), "q1": np.percentile(rtt_values, 25), "median": np.median(rtt_values), "q3": np.percentile(rtt_values, 75), "variance": np.var(rtt_values) } # Fit distribution to RTT data params = stats.lognorm.fit(rtt_values) pdf = stats.lognorm.pdf(x, *params)