Skip to content
Amida.
01 / HR analytics

HR Attrition & Workforce Dashboard

This project uses synthetic data. No real people or companies are represented. A synthetic 50-person workforce, cleaned with SQL, summarised in Excel and explored in an interactive dashboard.

Portfolio projectSynthetic dataHR analytics
  • Cleaned dataset
  • Data dictionary
  • SQL cleaning script
  • Excel summary (live formulas)
  • Interactive dashboard
  • One-page insights memo
01/ Problem

A fictional company, Vantara Global Services, lost 10 of its 50 employees in 12 months and wants to know where and why, using data it can trust.

02/ My role

Self-initiated portfolio project. All 50 records are synthetic. No real person or company is represented.

03/ Approach

I checked the raw records, cleaned them with SQL and validated the output against the Excel summary. I use the same cleaned records for every dashboard calculation and keep missing performance ratings as NULL.

04/ Deliverables

12 months to 30 September 2026

Workforce overview

Full synthetic workforce · n=50

90-Second Guided Tour

Guided tour (text). Not a video.

Headcount

50

Leavers

10

Attrition %

20.0%

Average engagement (1–5)

3.67

Average performance rating

3.31

n = people in each group. Attrition = leavers ÷ group headcount; this is a snapshot ratio, not an average-headcount annual rate.

Data-quality story

51 raw rows → 50 clean rows.

Duplicate row removed (1)

Department labels standardised (3)

Location labels standardised (3)

Date formats unified (3)

Gender codes standardised (1)

Blank performance ratings kept as NULL, not imputed (2)

SQL cleaning script

-- HR attrition dataset: raw -> clean (SQLite syntax, minor changes needed for MySQL or PostgreSQL)
-- Source: employees_raw (51 rows, SYNTHETIC data, snapshot date 2026-09-30)

DROP TABLE IF EXISTS employees_clean;

CREATE TABLE employees_clean AS
WITH dedup AS (                                   -- 1) remove exact duplicate employee_id rows
  SELECT *, ROW_NUMBER() OVER (PARTITION BY employee_id ORDER BY rowid) AS rn
  FROM employees_raw
)
SELECT
  employee_id,
  CASE                                            -- 2) standardise department labels
    WHEN LOWER(TRIM(department)) IN ('eng','engineering') THEN 'Engineering'
    WHEN LOWER(TRIM(department)) = 'hr'                   THEN 'HR'
    ELSE TRIM(department)
  END AS department,
  job_title,
  job_level,
  CASE LOWER(TRIM(location))                      -- 3) standardise location labels
    WHEN 'bangalore' THEN 'Bengaluru'
    ELSE UPPER(SUBSTR(TRIM(location),1,1)) || LOWER(SUBSTR(TRIM(location),2))
  END AS location,
  work_mode,
  CASE                                            -- 4) one date format (YYYY-MM-DD)
    WHEN hire_date LIKE '__-__-____'
      THEN SUBSTR(hire_date,7,4) || '-' || SUBSTR(hire_date,4,2) || '-' || SUBSTR(hire_date,1,2)
    ELSE hire_date
  END AS hire_date,
  tenure_months,
  age_band,
  CASE WHEN gender IN ('F','Female') THEN 'F'     -- 5) standardise gender codes
       WHEN gender IN ('M','Male')   THEN 'M' END AS gender,
  annual_ctc_inr,
  NULLIF(performance_rating,'') AS performance_rating,  -- 6) blanks -> NULL (not imputed)
  engagement_score,
  promoted_last_24m,
  overtime_flag,
  status,
  NULLIF(exit_date,'')   AS exit_date,
  NULLIF(exit_reason,'') AS exit_reason
FROM dedup
WHERE rn = 1;

-- Validation queries
SELECT COUNT(*) AS rows_after_cleaning FROM employees_clean;                       -- expect 50
SELECT COUNT(*) AS rows_before FROM employees_raw;                                 -- 51
SELECT COUNT(*) AS missing_ratings FROM employees_clean WHERE performance_rating IS NULL;  -- 2
SELECT department, COUNT(*) FROM employees_clean GROUP BY department;

What each step does

  • Dedupe with ROW_NUMBER: keep the first row per employee_id. This dataset has one exact duplicate; conflicting records would need a separate review.
  • Standardise text with CASE and TRIM: reconcile department labels, location names and gender codes.
  • Unify dates: convert DD-MM-YYYY to YYYY-MM-DD, keeping already standard dates.
  • Use NULLIF for blanks: preserve missing ratings and empty exit fields as NULL rather than inventing values.

Validation queries

QueryExpected result
SELECT COUNT(*) FROM employees_clean;50 rows after cleaning
SELECT COUNT(*) FROM employees_raw;51 rows before cleaning
SELECT COUNT(*) FROM employees_clean WHERE performance_rating IS NULL;2 missing ratings

Data dictionary · 18 columns

ColumnTypeDescriptionExample
employee_idtextUnique synthetic ID (E001-E050). No real person.E017
departmenttextEngineering, Customer Support, Operations, Sales, Finance, HR (standardised in cleaning).Engineering
job_titletextTitle implied by department and level.Senior Engineer
job_leveltextL1 (entry) to L5 (senior manager).L3
locationtextBengaluru, Hyderabad, Chennai, Kochi (standardised).Kochi
work_modetextHybrid, On-site or Remote.Hybrid
hire_datedate YYYY-MM-DDDate of joining (mixed formats fixed in cleaning).2024-05-02
tenure_monthsintegerMonths from hire_date to exit_date (leavers) or to snapshot date 2026-09-30 (active).29
age_bandtext22-25, 26-30, 31-35, 36-40, 41+.26-30
gendertextF or M (standardised). Used only to demonstrate an internal-equity check.F
annual_ctc_inrintegerSynthetic annual cost-to-company in INR.363000
performance_ratinginteger 1-5Latest rating. 2 blanks kept as NULL (not imputed).4
engagement_scoredecimal 1.0-5.0Synthetic pulse engagement score.3.2
promoted_last_24mY/NPromoted in the 24 months before snapshot.N
overtime_flagY/NRegular overtime worked (Y/N).Y
statustextActive or Left (left between 2025-10-01 and 2026-09-30).Left
exit_datedate YYYY-MM-DDBlank for active employees.2026-07-21
exit_reasontextBlank for active employees.Career growth

Source files

One-page insights memo

Synthetic dataFull synthetic workforce · n=50

10 of 50 employees (20%) left in the 12 months to 30 September 2026.

What the data shows

  1. 01

    Low engagement is the strongest signal. 7 of 12 employees scoring below 3.2 left (58%), against 3 of 38 (8%) of everyone else.

  2. 02

    Stalled careers. 9 of 38 employees not promoted in the last 24 months left (24%), against 1 of 12 who were promoted (8%).

  3. 03

    Overtime. 5 of 19 with regular overtime left (26%), against 5 of 31 without (16%).

  4. 04

    Reasons. Career growth (5) and manager relationship (4) account for 9 of the 10 exits. No leaver cited compensation.

  5. 05

    Where. Engineering lost the most people (4 of 16). Finance had the highest rate (2 of 6), on a very small base.

  6. 06

    Tenure. All 3 employees with under 12 months of service left (too few to generalise). Attrition was lowest at 12–47 months (3 of 28) and higher at 48+ months (4 of 19).

Recommendations

  • Run stay interviews within 30 days for anyone scoring below 3.2.
  • Add a promotion-readiness review at 18 months with a clear development path.
  • Flag employees with overtime in two or more consecutive months for a workload review.
  • Review one-to-one frequency and run upward feedback for managers named in exit interviews.
  • Add a structured 90-day check-in for every new joiner.

Limits

Synthetic data, n = 50. These are associations, not causes. Groups are small. Engagement was measured at one point in time, and leavers may have scored lower at exit. The pay table demonstrates a method and supports no conclusion. In a real company I would repeat this with a full year of data and test significance.

05/ What I learned / Limits

Synthetic data, n = 50. These are associations, not causes. Groups are small. Engagement was measured at one point in time, and leavers may have scored lower at exit. The pay table demonstrates a method and supports no conclusion. In a real company I would repeat this with a full year of data and test significance.

06/ In a live setting I would…

In a live setting I would repeat this with a full year of real data, test significance, and track the effect of the recommendations on attrition each quarter.

Explore another project →Labour Codes Brief