A Data Scientist at Syntes plays a pivotal role in bridging the gap between advanced predictive modeling, statistical rigor, and real-world business applications. Operating at the intersection of machine learning, data engineering, and business strategy, you will be responsible for transforming complex, multi-dimensional datasets into actionable insights. The models and analytical pipelines you build directly influence product development, operational efficiency, and strategic decision-making across global teams.
At Syntes, data science is not an isolated function. You will find yourself collaborating closely with cross-functional partners in Finance, Engineering, and IT Business Operations. This requires not only exceptional technical depth but also the ability to translate complex statistical outcomes into clear business logic. Whether you are optimizing core financial algorithms, implementing state-of-the-art large language models (LLMs), or refining statistical frameworks, your work will have a visible and immediate impact on the company's global footprint.
The environment is intellectually demanding and highly collaborative. Candidates who thrive here are those who possess a deep, first-principles understanding of machine learning algorithms, outstanding coding skills, and a genuine passion for solving ambiguous business challenges. The team values transparency, rigorous scientific methodology, and a practical approach to building scalable data products.
Initial Screening
reportedData Scientist covers at least four different jobs: experimentation, product analytics, causal work on observational data, and applied modelling that ships into a system. A screening call is the cheapest place to find out which of them is being hired for, and doing that diagnosis openly reads as senior rather than fussy. Ask what the last few pieces of work on the team actually were, and roughly how a week splits between querying, modelling and stakeholder time. Then say which parts of that you have done and which you have not. Claiming the whole range is the fastest way to be caught one round later.
What to demonstrate
- Whether you can distinguish the flavours of the role and locate your own experience inside one of them honestly
- Whether you name what you have not done instead of stretching to cover every line of the posting
- Whether your hard constraints (notice period, location, work authorisation, level) surface now rather than at offer stage
How to prepare
- Map the last two years of your time into rough percentages across query writing, experiment design, modelling and stakeholder work, so a question about scope has a real answer
- Mark every responsibility in the posting as done, adjacent or new, and prepare one sentence for each adjacent item naming the closest thing you have actually built
- Decide which logistics are non-negotiable before the call so you can state them in one sentence rather than negotiating live
Technical Evaluation
reportedA handful of shapes account for most of what gets asked in this format: a ranking or deduplication inside groups, a running or rolling total, a period-over-period comparison, and a cohort tracked forward over time. Recognising the shape quickly is most of the speed here; deriving it from scratch while a clock runs is where the time goes. Know that a window function keeps every row while a GROUP BY collapses them, and know which one the question needs. If the exercise is in Python instead of SQL, the same shapes arrive as groupby with transform, shift and merge, and the same grain mistakes are available.
What to demonstrate
- Whether you reach the right construct without a detour, such as ROW_NUMBER over a partition to deduplicate instead of a self-join against a MAX subquery
- Whether you know what your window frame actually is, since adding ORDER BY inside OVER changes the default frame and silently changes a running total
- Whether the thing runs. A near-miss that throws an error scores below a plainer query that returns the right rows.
How to prepare
- Write each of the four shapes once from memory against a small schema and keep the working version somewhere you will reread it: dedupe with ROW_NUMBER, a running total, a month-over-month change with LAG, and a retention table
- Compute one running total twice on data with tied timestamps, once on the default frame and once with ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, and look at where the two disagree
- If Python is on the table, rebuild the dedupe and the running total with groupby and cumsum, then assert the two implementations return identical rows
Panel Presentation
reportedWhere a loop includes a partner from outside the data team, that conversation usually carries the same weight as the technical ones and gets the least preparation. The person opposite you will not follow a derivation and does not need to. They are working out whether having you involved would make their decisions better or slower. The failure mode is not being too technical. It is answering a question about a decision with a description of your method, leaving the translation to them. What they carry into the debrief is the sentence you handed them, not the analysis underneath it.
What to demonstrate
- Whether a statistical result arrives as something the partner could act on, with the one caveat that would change their decision kept and the rest left out
- Whether you can state what you need from their side, in their terms: instrumentation that does not exist yet, a definition they own, or a holdout they have to agree to
- Whether uncertainty is given as a range someone can plan against, rather than as hedging that invites them to ignore the result
- Whether you ask what decision is actually on the table before explaining anything
How to prepare
- Take a result you know well and write the version for someone who stops reading after one sentence, then the three-minute version, and check the short one is not the long one with the qualifications stripped out
- For a past project, list everything you asked a non-technical partner for and how you phrased it, then rewrite each ask so it names what goes unmeasured without it
- Practise saying where a result does not apply, out loud, in one sentence that a partner could repeat accurately to someone else
3 candidate reports. Individual accounts describe a particular role and hiring cycle.
Syntes Software Engineer interview: two virtual discussions with hiring decision-makers
Both interviews were virtual and felt closely connected to the hiring decision. In the first video call, I spoke with the person directly involved in deciding who to hire. The second brought in additional team members for more of a panel discussion. We mainly talked about my background. In the second round, it felt like several people were considering my answers from different perspectives at onc…
Read full experienceSyntes Software Engineer interview: friendly start, no follow-through
The first thing that stood out was the lack of clarity once I finally had a chance to interview. Recruiters were direct when they reached out, but they gave little information about the process length and I received no updates afterward. In a separate attempt, I was contacted after applying for an assessment path. There was a quick phone call about my background and fit, with the hiring manager c…
Read full experienceSyntes Software Engineer interview: relaxed half-day panel session
Recruiter outreach came first, and they connected me with the team. The atmosphere was easygoing, and everyone felt friendlier than the people I had met at other European pharma companies. The process moved quickly into two rounds: a short call with the hiring manager, then, about a week later, a half-day in-person session. That session consisted of several panel-style conversations. Most intervi…
Read full experiencePracHub editorial advice for the preparation topics above.
Collapsing cancellation and payment failure into one churn number.
Involuntary churn from expired or declined payment instruments is a large and volatile share of gross churn, and it responds to retry schedules, card-updater coverage and billing provider, not to anything in the product. It also resolves late, so a period that looks involuntary today can be a successful retry next week, and reading the split before the dunning horizon closes overstates it. Compounding this, cancel-at-period-end means the cancellation request and the entitlement end are different timestamps on different rows, so a churn curve keyed on cancel_requested_ts and one keyed on churn_ts disagree by a full billing period.
Testing hours, revenue or completion with a difference in means on a heavy-tailed distribution.
Listening and viewing hours per account are strongly right-skewed and content popularity is close to power-law, so the variance of a sample mean is dominated by a few accounts and the central limit approximation converges slowly at realistic sample sizes. A t-test on mean hours can flip sign when one heavy account's week changes, and an experiment can appear significant because a single title released into one arm's window. Capping at a pre-registered percentile, or decomposing into a rate (did they stream at all) and a conditional intensity, controls the variance, at the stated cost that capping biases toward zero exactly when the true effect lives in the tail.
Comparing periods without accounting for seasonality or day-of-week
Compare whole weeks against whole weeks and check whether the same swing appeared in prior cycles or prior years before attributing it to anything you changed. Weekday and weekend populations often differ enough that a Tuesday-to-Saturday comparison is meaningless.
Naming a model class before naming the deployment constraints
Set out the latency budget, the label delay, the retraining cadence, the interpretability requirement and the number of labelled examples, then pick the model that fits them. A boosted-tree answer to a problem where each decision must be explained to the affected user is a well-executed answer to the wrong question.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Qualified hours per active account-week from its written definition
You are given fct_stream as a pandas frame (stream_id, account_id, started_at in UTC, played_seconds, is_qualified) and dim_account (account_id, is_test_account), plus an as-of timestamp. Implement this metric exactly: the numerator is sum(played_seconds)/3600 over qualified streams started inside the window; the denominator is the count of distinct (account_id, ISO week) pairs with at least one qualified stream in that week; the window is the four most recent ISO weeks that are complete as of the given timestamp. Exclude test accounts. Return the scalar and the four per-week rows.
Approach
- Drop test accounts first, then filter to is_qualified. The denominator is defined on qualified streams, so filtering after grouping quietly lets an account-week in on the strength of an unqualified stream.
- Derive the ISO key with started_at.dt.isocalendar() and keep BOTH iso_year and iso_week. The week number alone collides every January and silently merges two different years of a week into one cell.
- Fix the window from the as-of timestamp rather than from max(started_at): a week is complete only when its Monday 00:00 UTC plus seven days is at or before the as-of instant, which is what excludes the partially observed current week.
- Aggregate once: sum played_seconds for the numerator, and take the number of unique (account_id, iso_year, iso_week) tuples for the denominator, not the number of unique accounts.
- Emit the four per-week rows as well, and confirm the scalar equals the account-week-weighted mean of the four weekly ratios.
Follow-up
- A profile-level version of this metric reads lower per unit than the account-level one. Which would you use for a personalisation readout, and what does each unit actually aggregate?
- Offline playback is uploaded in bulk days after it happened. What does that do to the most recent complete week, and how would you decide when a week is safe to publish?
- How would this metric move if the qualification threshold rose from 30 to 60 seconds, separately for a short-form-heavy and a long-form-heavy catalogue?
Five invariant checks over a playback fact table
fct_stream arrives with stream_id, started_at, ended_at (nullable), played_seconds, max_position_seconds, completion_ratio, is_qualified, event_date, and duration_seconds joined from the content dimension (null for live events). Write one function returning a tidy frame with a row per rule: rule name, eligible rows, violations, violation share, one example stream_id. The five rules are duplicate stream_id; played_seconds exceeding wall-clock ended_at minus started_at; max_position_seconds exceeding duration_seconds; completion_ratio disagreeing with max_position_seconds divided by duration_seconds; and event_date not equal to the UTC date of started_at.
Approach
- Give every rule its own eligible-row mask before its violation mask. Rules 2 to 4 are undefined where ended_at or duration_seconds is null, and a comparison against NaN evaluates False, so a mostly-null column otherwise reports a clean bill of health.
- Compare the recomputed completion_ratio with a tolerance (absolute difference above 1e-6), never with ==. It is stored as a float division and exact equality fails on rows that are correct.
- Allow a small tolerance on rules 2 and 3 too: a final segment can carry the playhead a second or two past duration_seconds, and heartbeat timestamps come off client clocks. State the tolerance you chose rather than burying it.
- Return violations and eligible rows as separate columns so the share has a stated denominator, and carry one example stream_id per rule so the output is actionable rather than a number.
- Read rule 5 as a signal rather than corruption: event_date drifting from the UTC date of started_at is the signature of offline playback uploaded after the partition closed, and its distinct dates are the recompute list.
Worked solution 20 min
- Write a helper taking (name, eligible_mask, violation_mask) and returning one result row, which forces every rule to declare its eligible population.
- Rule 1: duplicated('stream_id', keep=False), eligible is all rows. Rule 5: event_date not equal to started_at.dt.date, eligible is all rows.
- Rules 2 to 4: eligible masks are ended_at.notna() and duration_seconds.notna(); violation masks are played_seconds above wall_seconds plus 2, max_position_seconds above duration_seconds plus 2, and the absolute completion_ratio difference above 1e-6.
- Take one example stream_id per rule with .loc[mask].head(1), guarding the empty case so a clean rule does not raise.
- Concatenate the five rows and sort by violation share descending.
Follow-up
- Rule 5 fires on 0.4 percent of rows, all from one app_version, all with reported_at days after started_at. Is that a bug, and what do you do about the daily numbers already published?
- How would you turn these into a blocking check in the pipeline without failing the load every time one client version misbehaves?
- Which of the five would you expect to fire on live events specifically, and how do you keep them out of the denominator?
Attribute streams to prior impressions without merge_asof
You have impressions (impression_id, profile_id, content_version_id, rendered_at, surface, slate_position, viewport_visible_ms) and streams (stream_id, profile_id, content_version_id, started_at). Attribute each stream to the most recent impression of the same profile and content version with rendered_at at or before started_at, a gap of at most 30 minutes, and viewport_visible_ms above zero; break ties on rendered_at by the smaller slate_position. pd.merge_asof and any groupby-apply over individual rows are off limits. Return streams with impression_id added, plus the attributed share by surface.
Approach
- Filter impressions to viewport_visible_ms above zero first. A row that never scrolled into view is not an exposure, and leaving it in lets an unseen impression win the as-of match over a seen one.
- Stack both frames into one long frame with a ts column and an is_stream flag, then sort by (profile_id, content_version_id, ts, is_stream, negative slate_position) so that at an identical timestamp impressions sort before the stream, and among tied impressions the smallest slate_position sorts last.
- Forward-fill the candidate impression_id and rendered_at with groupby(['profile_id','content_version_id']).ffill(). The grouping is what stops the fill leaking across profiles; a global ffill on a sorted frame is the usual wrong answer.
- Apply the 30-minute window as a post-filter on the filled candidate, and let a stream that fails it go unattributed rather than falling back to an older row. Every candidate precedes the stream, so the gap grows monotonically as you walk backwards: if the nearest in-view impression is more than 1800 seconds old, no earlier one is inside the window either, and null is the correct answer. The window cannot be pushed onto the impressions frame beforehand in any case, because "within 30 minutes" is defined against a particular stream's started_at and one impression is a candidate for many streams. Dropping impressions rendered more than 30 minutes before the earliest stream is a safe prune, but it is an optimisation, not the rule.
- Slice the stream rows back out and compute the attributed share by surface, stating that unattributed streams (resume, direct link, radio seed) are not a surface and belong in their own bucket.
Follow-up
- The same item was rendered in two slates inside the window. Your tie-break picks one. What does that do to per-surface credit, and what would a fractional rule change?
- How would you validate this against the impression_id already carried on fct_stream, and what would a systematic disagreement tell you?
- At 2 million impressions and 500 thousand streams this fits in memory. What changes at 200 million?
Pick one canonical version per work without losing remaster plays
dim_content_version has content_version_id, content_id (stable across versions of the same work), title, content_type, duration_seconds, catalogue_added_ts, is_active_version and updated_at. A remaster, re-edit, dub or re-encode is a new row sharing content_id, and more than one row per content_id can carry is_active_version = true. Produce exactly one canonical row per content_id, preferring is_active_version = true, then the latest catalogue_added_ts, then the highest content_version_id as a deterministic tie-break. Then join fct_stream (content_version_id, played_seconds, is_qualified, started_at) and report qualified hours per content_id for last month.
Approach
- ROW_NUMBER() OVER (PARTITION BY content_id ORDER BY is_active_version DESC, catalogue_added_ts DESC NULLS LAST, content_version_id DESC), keeping rn = 1. The third sort key exists only so two runs return the same row; without it the winner among ties is whatever the engine happened to produce, and the result diffs against yesterday for no reason.
- Spell NULLS LAST out. In PostgreSQL, DESC defaults to NULLS FIRST, so a version with no catalogue_added_ts would win the canonical slot ahead of every dated one. Other engines order NULLs differently, which is exactly why the clause belongs in the query rather than in your head.
- Aggregate the streams first, at content_id grain, by joining fct_stream to dim_content_version on content_version_id and grouping on content_id. Every version's plays belong to the work.
- Only then attach the canonical attributes, with a join from the aggregate to the rn = 1 CTE on content_id. Inner-joining fct_stream to the canonical row instead discards every play of every non-canonical version, which is most of the catalogue's remasters and all of its dubs.
- Report the count of content_ids carrying more than one is_active_version row as a data-quality line beside the result, since that condition is what made the dedup necessary and its size decides whether this is a query fix or a pipeline fix.
Worked solution 25 min
- Size the problem before solving it: count content_ids with more than one row, and separately those with more than one is_active_version = true row.
- Write the ROW_NUMBER CTE with explicit NULLS LAST, keep rn = 1, and assert COUNT(*) equals COUNT(DISTINCT content_id) in the dimension.
- Aggregate fct_stream to content_id through the version dimension, filtering is_qualified and last month, as SUM(played_seconds)/3600.
- Join the canonical attributes on and compare the grand total against the same aggregate computed at version grain.
Follow-up
- Which grain should hours be reported at, content_id or content_version_id, and who is the consumer of each?
- Rights expire per version, so one version's window can be closed while another is live. What does your canonical pick then mean for an availability report built on the same CTE?
Paid retention triangle with monthly and annual curves kept apart
fct_subscription_period has subscription_period_id, account_id, period_index, billing_interval, period_start_ts, period_end_ts, payment_status, cancel_requested_ts and renewal_outcome. dim_account has account_id, first_paid_ts and is_test_account. Build a paid-retention triangle: label non-test accounts with the calendar month of first_paid_ts, and for month offsets 0 through 6 report the share of the cohort still holding a period with payment_status in ('paid','retried_paid') that contains the instant first_paid_ts + offset months. The anchor is each account's own first payment, not the first day of the offset calendar month. Report monthly and annual billing_interval as separate curves. Exclude cohorts not yet mature at offset 6.
Approach
- Take the cohort key from dim_account.first_paid_ts, not from MIN(period_start_ts). The period table includes trial periods as zero-amount rows with payment_status = 'trial_no_charge', so the minimum start shifts trialling accounts a cohort early and inflates the youngest cohort's offset-0 denominator.
- Anchor every offset on that same first_paid_ts; the calendar month is only the row label on the triangle. Anchoring survival on the first day of the offset month instead breaks offset 0 for everyone who did not first pay on the 1st — an account that first paid on the 20th holds no paid period covering that month's 1st, so its offset-0 cell reads as churned and the curve rises from offset 0 to offset 1. A retention chart whose first step goes up is almost always this. Two conventions to write down while you are here: PostgreSQL clamps timestamp + INTERVAL '1 month' at month end, so an account first paid on the 31st is tested on the 28th or 30th at some offsets; and the whole construction assumes first_paid_ts falls inside its own first paid period, which fails if the platform charges shortly before period_start_ts, in which case anchor on that period's period_start_ts instead.
- Build the offsets 0..6 as an explicit ordered set and cross-join it to the cohort list. A grid built by aggregating survivors alone loses any (cohort, offset) cell with no survivors, so a collapsing cohort reads as a missing row instead of a zero.
- Test survival as interval containment, not date equality: EXISTS a period for that account with payment_status in ('paid','retried_paid') where first_paid_ts + offset months falls inside [period_start_ts, period_end_ts). One annual row contains seven consecutive anniversaries on its own; an equality join against period_start_ts finds it at offset 0 and nowhere else.
- Partition everything by billing_interval and never pool. An annual account has had no opportunity to churn before day 365, so pooling makes an annual-heavy cohort read as retentive when what it actually is, is un-renewed. billing_interval lives on the period and not on the account, so take the cohort's interval from the period containing first_paid_ts and hold it fixed for all seven offsets; an account that switches monthly to annual at offset 4 otherwise appears in both curves and is counted twice in the denominators.
- Apply maturity last, and against the anchor rather than the label: keep a cohort only when the last day of that cohort month plus six months is at or before the last fully closed day in the data, so every account in it has actually reached its offset-6 anniversary. Otherwise the newest cohorts print 0% at the far offsets for a reason that is entirely calendar.
Follow-up
- An account migrates plan mid-cohort with renewal_outcome = 'migrated_plan'. Retained or churned, and what does your choice do to the revenue story told beside this chart?
- A period fails and a retry succeeds as a new row. Does your survival test find the account at that offset, and should it?
- Can you produce a curve for annual accounts that is comparable to the monthly one before twelve months have elapsed, and what do you give up?
Publish a per-person metric when no table holds a person
Leadership asks for streams per user per week on a weekly dashboard. You have dim_account (account_id, plan_tier, max_profiles), dim_profile (profile_id, account_id, profile_type, personalisation_opt_out, last_active_ts) and fct_stream (profile_id, account_id, device_type, started_at, played_seconds, is_qualified). Neither table holds a person. Deliverable: state the unit you will publish, define the metric fully, name what that unit actually aggregates, and give one diagnostic that tells the dashboard's readers how much of the data has no identifiable person behind it.
Approach
- Establish that both available units are wrong in different directions: an account is a household so it aggregates several people, and a profile is not reliably one person either because profiles are frequently not switched and shared-screen, car and speaker playback lands on whatever profile was last active or on a default.
- Pick the account as the published unit and justify it by what the number will be used for: billing, churn and revenue denominators are all per account, so an account-grained engagement metric joins to every other number on the dashboard without an impedance mismatch, while a profile-grained one silently changes the denominator between panels.
- Define it fully: qualified hours per active account-week, numerator sum(played_seconds)/3600 over is_qualified rows, denominator distinct (account_id, ISO week) pairs with at least one qualified stream, is_test_account excluded, window the four most recent complete ISO weeks, with the account-level two-distinct-date rule used for the active-account count.
- Label what the unit aggregates directly on the dashboard, not in documentation: the figure is hours per subscribing household per week, and family and duo plan_tier accounts read higher for composition reasons and are not more engaged people.
- Give the diagnostic that makes the ambiguity visible: share of qualified hours on profile_type = 'default_unnamed' plus the share on device_type in ('smart_speaker','car','smart_tv'), published beside the headline, since that is the fraction of consumption whose person is genuinely unknown and it tells a reader how much interpretive weight the number bears.
- Rule out the false fix explicitly: never divide account hours by max_profiles, because max_profiles is a plan entitlement that does not vary with household size and dividing by it manufactures a per-person figure that is a deterministic function of the plan.
Follow-up
- Personalisation claims a win measured at the profile level. What do you need to know before you believe it is a person-level effect and not a device or context effect?
- A cut of the dashboard by plan_tier shows family accounts at three times individual accounts on hours. Write the sentence you would put under that chart.
- If you could add one column to dim_profile to make this less ambiguous, what would it be and what would it still fail to tell you?
Value a catalogue removal with synthetic control
A non-exclusive licence covering 1,400 content_version rows expires on a fixed date in six territories and is renewed in the other 48. Nothing else about the product changes that week. Using fct_stream, dim_content_version.available_territories and fct_subscription_period, estimate the removal's effect on qualified hours per active account-week and on 90-day voluntary churn. No randomisation is available and the six territories were chosen by the counterparty. Specify the estimator, the control group, the assumption you need and how you would test it.
Approach
- Set the panel: one row per territory-week of qualified hours per active account-week, at least 24 pre-expiry weeks, with the six affected territories treated and the 48 renewed ones as the donor pool.
- Reject plain difference-in-differences as the primary estimator. The six territories were selected by a counterparty, not at random, and territory-level hours differ by an order of magnitude, so an unweighted parallel-trends claim is not credible. Fit it as a secondary event study with leads and lags instead.
- Fit a synthetic control per treated territory, with donor weights chosen to match the pre-period outcome path plus the predictors that matter here: plan-tier mix and the pre-period share of hours coming from the affected rights_holder_id. Aggregate the six gaps afterwards rather than pooling territories first.
- Clean the donor pool before fitting. Exclude any donor where the same licence or another material rights window changes inside the event window; a contaminated donor is the failure mode that quietly halves the estimate.
- Do inference by placebo-in-space: refit the synthetic control treating each donor as if it were treated, rank the true post-to-pre RMSPE ratio within that distribution, and report the exact p-value, whose floor with 48 donors is 1/49 = 0.020.
- Split the answer into mechanical and behavioural. The removed versions' pre-period share of qualified hours is the gross loss, the synthetic-control gap is the net, and one minus net over gross is the substitution rate. Read churn only on monthly-billing accounts and declare the annual cohort censored, since an annual account has no renewal decision inside a 90-day window.
Worked solution 45 min
- Build the territory-week panel from fct_stream using play_territory as the unit, restricted to is_qualified = true, spanning 24 weeks before and 12 weeks after the expiry date, with the active-account denominator computed identically in every territory.
- Compute gross exposure per treated territory: the share of pre-period qualified hours on content_version rows whose available_territories included that territory and whose rights_expires_at is the expiry date.
- Fit the synthetic control per treated territory on the pre-period and read the post-period gap; also fit a two-way fixed-effects event study with leads from week minus 12 to minus 1 and lags to plus 12.
- Run 48 placebo-in-space fits and rank the treated post-to-pre RMSPE ratios against that distribution.
- Repeat the whole procedure with a placebo expiry date 12 weeks earlier, where the estimated effect must be zero.
- Build the churn read separately on monthly-billing accounts with period_end_ts inside the 90-day window and renewal_outcome = 'cancelled_voluntary', computed only after the dunning horizon has closed.
Follow-up
- Two of the six territories account for 80 percent of the affected hours. Does aggregating the six gaps into one number still mean anything?
- The pre-period fit is poor for one territory. Do you drop it, report it, or widen the donor pool, and what does each choice do to the placebo inference?
- Management wants a renewal decision for next year. Which number do you hand them, and what does it assume about a different catalogue and a different competitive set?
Involuntary churn share jumped after a dashboard rebuild
Involuntary share of gross churn — fct_subscription_period rows with renewal_outcome = 'failed_involuntary' over rows with renewal_outcome in ('cancelled_voluntary','failed_involuntary'), keyed on period_end_ts month — reads 31% for last month against 19% for the three months before. Billing reports no change in decline rates and dunning_attempts per failed period is unchanged. Using fct_subscription_period (period_end_ts, renewal_outcome, payment_status, dunning_attempts, cancel_requested_ts, billing_provider), determine whether involuntary churn actually rose. Deliverable: the corrected month-on-month series and the specific change that produced the step.
Approach
- Check the month's maturity before anything else. Periods ending recently still have retries in flight, and a row reading 'failed_involuntary' today becomes payment_status = 'retried_paid' next week. Recompute every one of the last twelve months as of a fixed age — period_end_ts plus the maximum retry window — instead of as of today.
- If the step survives age alignment, hunt for a definition change: diff the model or query against the version that produced 19%, and check specifically whether the month is now keyed on cancel_requested_ts rather than period_end_ts, and whether 'refunded' or 'chargeback' rows have entered one side of the ratio.
- Settle it with one query rather than a narrative. Run the old definition and the new definition on the same month. Two numbers from one month under two definitions is decisive; two numbers from two months under one definition is not.
- If a definition did change, restate history rather than leaving a step in the chart: run the new definition back across all twelve months and publish both series with the cutover marked, so nobody re-discovers this in a quarter.
- Cut the horizon-closed series by billing_provider before concluding nothing moved. Card-direct, app-store and carrier billing have different retry mechanics and different success rates, so a mix shift between them moves the ratio with every provider's own rate flat.
Follow-up
- How would you set the dunning horizon from the data instead of taking billing's stated maximum?
- Under cancel-at-period-end, the cancellation request and the entitlement end fall in different months. Which timestamp keys this metric, and what does the other choice do to the curve?
Instead of guessing where the week should go, day one measures it under a fixed rubric and allocates the remaining hours in proportion to the gaps. The method is deliberately rigid: the allocation is written down before any studying starts and is not renegotiated when a topic turns out to be unpleasant.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Diagnostic, scored before you study anything
- Sit a 100-minute timed diagnostic in four blocks: 30 minutes of SQL across three prompts, 25 minutes of short-answer statistics, 25 minutes on one modelling or case prompt, and 20 minutes delivering one behavioural story aloud.
- Score each block from 0 to 3 on a fixed rubric where 3 is correct and fluent, 2 is correct but slow or prompted, 1 is partially correct, and 0 is stuck, grading the output rather than how the attempt felt.
- Allocate the hours for days two to five roughly in proportion to 3 minus the score in each block, write the allocation down, and commit to not revising it midweek.
Deliverable: A scored rubric and a fixed hour allocation for the rest of the week.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Largest gap: find the boundary rather than the subject
- Break the weakest area into five named sub-skills (for query work: grain control, window frames, date arithmetic, set logic with NULLs, and reading a query plan) and rate each one, so the rest of the week targets a sub-skill instead of a subject.
- Solve three problems chosen to sit just above where the rating drops off, and for each write the first move you failed to make.
- Re-solve one of them from memory four hours later, on paper, with nothing open.
Deliverable: A five-item sub-skill map with the two blocking sub-skills circled.
Practice prompt ↗Practice prompt ↗03Largest gap: drill the blocking sub-skill
- Do eight short repetitions of the same shape rather than eight different problems, so what you practise is the pattern and not the puzzle.
- Write the rule you now hold in one sentence, then test it against a case built to break it: a ranking function over a column with ties, or a two-sample test on observations that are obviously dependent.
- Have someone else read your one-sentence rule and find the precondition you left out.
Deliverable: One rule statement with its preconditions attached and one counterexample that would have caught the incomplete version.
Practice prompt ↗Practice prompt ↗04Second gap, plus maintenance on your strongest area
- Run the same sub-skill map and boundary protocol on the second-largest gap, compressed into half the day.
- Spend 25 timed minutes on your strongest area to stop it decaying, choosing the hardest problem you can still finish rather than an easy warm-up.
- Compare how the two areas fail: whether you lose time on recall, on setup, or on arithmetic, because the fix differs for each.
Deliverable: A second sub-skill map plus a one-line diagnosis of how each area fails you.
Practice prompt ↗Practice prompt ↗Worked solution ↗05The gap that is not a skill
- Record yourself answering one technical and one behavioural prompt, then count two things in the playback: how many seconds before your first clarifying question, and how many sentences you started without knowing where they ended.
- Rewrite your three most-used stock phrases into shorter versions, and practise saying "I do not know, here is how I would find out" without softening it into a guess.
- Deliver one answer again with a hard 90-second limit to force structure before detail.
Deliverable: Two recordings with a counted improvement in time-to-first-question.
Practice prompt ↗06Retest under day-one conditions
- Sit the same 100-minute diagnostic structure with new prompts of comparable difficulty and score it on the identical rubric.
- Compare block by block, and for any block that did not move, change the method rather than adding hours: a block stuck at 1 usually means the practice was too varied, not too short.
- Write which single block you would still lose the offer on.
Deliverable: A second scored rubric placed next to the first, with one named remaining risk.
Practice prompt ↗07Full loop under interview conditions
- Run a 60-minute mock covering the two blocks that moved least, with an interviewer instructed to interrupt and change direction.
- Write your recovery script for the moment you go blank: restate the question, state your assumption, name the first thing you would check.
- Reduce the week to the rule statements you wrote, each with its preconditions attached, then say every one of them out loud without reading it and cut any you cannot state in a single sentence, since a rule you have to reconstruct mid-answer will not survive being interrupted.
Deliverable: A one-page card holding the recovery script and only the rules you could state from memory.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
A number you shipped turned out to be wrong, and someone had already acted on it. That is one of the most useful stories a data person can carry. What is being scored is how fast you noticed, who you told first, and what you changed in the process so the same class of error could not repeat quietly.
Scope a one-line request about home row performance
A director messages you: "Is the new home row working?" Nothing else. A new ranker_version has been serving a fraction of profiles for eleven days. You have fct_impression (surface, slate_position, ranker_version, is_exploration_slot, logging_propensity, experiment_assignment_id, was_clicked) and fct_stream (impression_id, start_source, is_qualified, played_seconds). You get a fifteen-minute call before they go into a rollout meeting. Deliverable: the three questions you ask before writing any SQL, the single primary metric you commit to with its guardrail, and the questions you tell them this data cannot answer.
Approach
- The probe is whether you convert a vague request into a decision before producing a number. Ask what happens at each answer — rollback, widen, iterate — because a question whose answer changes nothing is a report request and should be scoped as one.
- Pin the unit of analysis out loud. fct_impression is at (profile, slate, slot) grain and experiment_assignment_id is per assignment, so the comparison must be aggregated to the assignment unit first; comparing impression-level rates lets a change in slate length move the metric on its own.
- Commit to one primary metric from the tree — qualified hours per active account-week for assigned accounts — and name the guardrail pair explicitly: share of qualified streams with start_source = 'autoplay_continuation', and median completion_ratio within content_type. A ranker can lift qualified stream counts by queueing short items that clear the 30-second threshold, and the guardrail is the only thing that catches it.
- State the refusals with structural reasons, not time reasons: eleven days gives no matured cohort, so month-6 retention and net revenue per active account-month are unanswerable; and logging_propensity is populated only where is_exploration_slot = true, so the positivity condition for an off-policy estimate fails outside those slots.
- Write the scope back in one paragraph — decision, metric, guardrail, the date the read becomes valid — and get it agreed in the thread before querying, so the number that arrives is the number that was asked for.
Follow-up
- They reply "just give me click-through by slate position." What do you say, and what would that number actually tell them?
- The eleven days include a weekend and a large release landing on day six. Does that change the metric you commit to, or only the read date?
- What would have to be true for you to be willing to answer the retention question from this experiment?
State the impact of your last year without inflation
You are asked what your work was worth over the last year. Two candidates are on the table. First: a dunning-schedule change you analysed, shipped in March with no holdout, after which the involuntary share of gross churn fell three points. Second: a metric-definition change you drove, which nobody can attribute revenue to. Deliverable: the impact claim you make for each, the counterfactual attached to each, and which one you lead with in a performance review — with the reason.
Approach
- The probe is whether you apply the same causal standard to your own work as to a product experiment. Attach the counterfactual before the claim: involuntary share of gross churn responds to card-updater coverage, billing_provider mix, retry schedules and the dunning horizon you read it at, any of which can move three points with the schedule change contributing nothing.
- Say which evidence would separate those, then say plainly which you actually have. A staggered rollout by billing_provider, a holdout, or at minimum a flat pre-period series would each support a different strength of claim; with none of them, the honest claim is a contribution to a favourable movement, stated as such.
- Check the classification itself before claiming the movement is real: the split between cancelled_voluntary and failed_involuntary must be computed only after the dunning horizon has closed for every period in the month, or retries still in flight are counted as churn and the share reads too involuntary in one direction and corrects in the other.
- Price your contribution rather than the outcome. The value of analysis that changed a decision is the decision's delta multiplied by the probability the decision would not have been taken without it, and being explicit about that second factor is most of what honesty means here.
- Make the definition-change claim concrete rather than apologetic: name the decisions that would have been taken on the wrong number, the reports it reconciled, the recurring argument it closed, with dates. Then lead with whichever claim survives questioning, not whichever carries the larger number, because an inflated first claim makes the second unbelievable.
Follow-up
- Your manager writes up the churn improvement and credits it to you. Do you correct it, and to whom?
- How would you have designed the March rollout so that attribution was possible, at what cost in delay?
- Name something you worked on last year that had no impact, and say what you learned from that rather than from the wins.
Announce a stream-definition change that shifts payouts
You find that the 60-second idle gap used to sessionise heartbeats into fct_stream rows splits one continuous listen into two streams whenever a phone backgrounds briefly on cellular. Correcting the gap lowers qualified stream counts on phones by an estimated four percent; total played_seconds is unchanged. Per-stream counts drive rights-holder payout shares. Deliverable: what you verify before telling anyone, the order in which you take it to the engineering owner, finance and content partnerships, and your recommendation on restating history.
Approach
- The probe is whether you can tell a technical correction from a commercial decision and keep them apart in the room. Verify the split streams are genuinely one listen before anything else: same profile_id, same content_version_id, contiguous max_position_seconds across the boundary, and a gap distribution with a spike at the background-timeout duration rather than a smooth tail.
- Compute the distributional effect, not the average. The four percent aggregate is not what anyone will argue about; recompute under the corrected gap and report which rights_holder_id groups gain and lose share, because short-form catalogue on mobile is where the splits concentrate and that is not spread evenly across counterparties.
- Defend the new rule on its own terms rather than on the direction of the number. The idle gap is a choice, so the argument is evidence that the two rows describe one continuous listen — never that the corrected count is lower and therefore more conservative, which invites the symmetric accusation next time the fix goes the other way.
- Name the ownership boundary out loud: engineering owns the sessionisation rule, finance and partnerships own whether payouts are restated. Conflating them is how a correct fix gets blocked by a commercial objection it should never have been exposed to.
- Sequence the conversations so the number stops moving before it leaves the building: engineering owner first to confirm the rule and land the fix, finance second to size the restatement, partnerships last. Recommend restating history for internal metrics so trends stay comparable, and recommend against retroactive payout adjustment unless the contracts require it — naming who must answer that contractual question rather than answering it yourself.
Follow-up
- Partnerships asks you to hold the fix until after the quarter closes. What do you do, and who else needs to know you were asked?
- How do you present a change that raises some counterparties' shares and lowers others', in the same meeting, to people who will compare notes afterwards?
- The four percent estimate itself has an interval spanning roughly two to seven percent. Does that change the recommendation or only the sequencing?
- 01
A director messages you: "Is the new home row working?" Nothing else. A new ranker_version has been serving a fraction of profiles for eleven days. You have fct_impression (surface, slate_position, ranker_version, is_exploration_slot, logging_propensity, experiment_assignment_id, was_clicked) and fct_stream (impression_id, start_source, is_qualified, played_seconds). You get a fifteen-minute call before they go into a rollout meeting. Deliverable: the three questions you ask before writing any SQL, the single primary metric you commit to with its guardrail, and the questions you tell them this data cannot answer.
- 02
You are asked what your work was worth over the last year. Two candidates are on the table. First: a dunning-schedule change you analysed, shipped in March with no holdout, after which the involuntary share of gross churn fell three points. Second: a metric-definition change you drove, which nobody can attribute revenue to. Deliverable: the impact claim you make for each, the counterfactual attached to each, and which one you lead with in a performance review — with the reason.
- 03
You find that the 60-second idle gap used to sessionise heartbeats into fct_stream rows splits one continuous listen into two streams whenever a phone backgrounds briefly on cellular. Correcting the gap lowers qualified stream counts on phones by an estimated four percent; total played_seconds is unchanged. Per-stream counts drive rights-holder payout shares. Deliverable: what you verify before telling anyone, the order in which you take it to the engineering owner, finance and content partnerships, and your recommendation on restating history.
Is this an official Syntes interview guide?
No. It is PracHub's own research and practice material for the Data Scientist role at Syntes. Rounds and questions reflect what candidates have reported, not a process Syntes has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How difficult is the Data Scientist interview at Syntes?
The interview process is generally rated as average to difficult. It is highly technical and requires a deep, first-principles understanding of your past work and core machine learning concepts. It is not a process you can pass through memorization; practical application and clear communication are key.
PracHub interview research ↗What is the typical timeline from the first screen to an offer?
Because the process involves multiple rounds and various stakeholders (including senior leadership and cross-functional partners), the timeline typically spans 4 to 8 weeks. Delays can occasionally occur during holiday seasons or when coordinating large panel presentations.
PracHub interview research ↗How much coding should I expect in the interview?
You should expect at least one dedicated live coding round focusing on SQL and algorithmic problem-solving in Python or R. Additionally, you must be prepared to discuss the software engineering aspects of your past projects, including code structure and deployment.
PracHub interview research ↗Is there an emphasis on product and business sense?
Yes. Syntes values data scientists who can connect their models to business outcomes. You will face questions designed to test how you apply analytics to solve business logic problems and how you measure the success of your models in financial or operational terms.
PracHub interview research ↗Sources & methodology 3 sources ↗
Official role evidence, timestamped platform data and clearly labeled preparation advice.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
platform · Accessed 2026-09-22 - 02PracHub Data Scientist practice ↗
Cross-company practice questions for this role.
platform · Accessed 2026-09-22 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
platform · Accessed 2026-09-22