Projekt Zespolowy HR

2025-01-31

Wstęp

W dzisiejszym dynamicznym świecie korporacyjnym, zrozumienie i przewidywanie odejść pracowników, określane w naszej analizie jako attrition, staje się kluczowe dla utrzymania stabilności i wzrostu organizacji. W niniejszym projekcie zbadaliśmy wpływ zmiennych w organizacji (np. stawka godzinowa, dział) oraz zmiennych osobistych (stan cywilny, odległość od pracy) na attrition.

Naszym celem była identyfikacja najbardziej istotnych czynników wpływających na decyzję pracowników o zmianie firmy. Wierzymy, że wyniki tego projektu mogłyby pomóc lepiej zrozumieć przyczyny takich decyzji oraz zmniejszyć rotację w firmie.

library(dplyr)
library(readr)
HR=read.csv("HR.csv")

Data Cleansing, Wrangling

Poprzez dokładne czyszczenie danych, w tym usunięcie zbędnych kolumn oraz duplikatów, a także adresowanie brakujących i odstających wartości, tworzymy bazę do naszej analizy.

Usuwanie zbędnych kolumn

Ze względu na brak zróżnicowania w kolumnach ,,EmployeeCount”, “Over18” i ,,StandardHours”, usuwamy je z naszego zbioru danych.

HR <- HR %>% select(-EmployeeCount, -Over18, -StandardHours) 

Wykrycie duplikatów

Jak można zauważyć, w naszym zbiorze danych nie wykryliśmy żadnych duplikatów, czyli wszystkie dane dotyczą innych osób

duplikaty <- HR[duplicated(HR) | duplicated(HR, fromLast = TRUE), ]
duplikaty
##  [1] Age                      Attrition                BusinessTravel          
##  [4] DailyRate                Department               DistanceFromHome        
##  [7] Education                EducationField           EmployeeNumber          
## [10] EnvironmentSatisfaction  Gender                   HourlyRate              
## [13] JobInvolvement           JobLevel                 JobRole                 
## [16] JobSatisfaction          MaritalStatus            MonthlyIncome           
## [19] MonthlyRate              NumCompaniesWorked       OverTime                
## [22] PercentSalaryHike        PerformanceRating        RelationshipSatisfaction
## [25] StockOptionLevel         TotalWorkingYears        TrainingTimesLastYear   
## [28] WorkLifeBalance          YearsAtCompany           YearsInCurrentRole      
## [31] YearsSinceLastPromotion  YearsWithCurrManager    
## <0 wierszy> (lub 'row.names' o zerowej długości)

Wartości brakujące

Do identyfikacji wartości brakujących użyliśmy biblioteki naniar. Przy użyciu funkcji vis_miss oraz gg_miss_var przygotowaliśmy wizualizację brakujących danych w naszym zbiorze. Przeanalizowaliśmy również procent brakujących danych.

Każdy z powyższych kroków jest kluczowy dla uzyskania miarodajnych wartości i wiarygodnych wyników analizy.

library(naniar)
vis_miss(HR)

miss_var_summary(HR)
## # A tibble: 32 × 3
##    variable         n_miss pct_miss
##    <chr>             <int>    <num>
##  1 Attrition           150    10.2 
##  2 MonthlyIncome       150    10.2 
##  3 Age                 100     6.80
##  4 BusinessTravel        0     0   
##  5 DailyRate             0     0   
##  6 Department            0     0   
##  7 DistanceFromHome      0     0   
##  8 Education             0     0   
##  9 EducationField        0     0   
## 10 EmployeeNumber        0     0   
## # ℹ 22 more rows
gg_miss_upset(HR)

gg_miss_var(HR)

Zmienną Attrition zapisaną jako zwykły test zmieniliśmy na typ factor.

HR$Attrition <- factor(HR$Attrition, levels = c("No", "Yes"))

Imputacja danych

Do imputacji danych wykorzystaliśmy metodę wielokrotnej imputacji przy użyciu biblioteki mice. Umożliwiło nam to estymację brakujących wartości na podstawie pozostałych zmiennych oraz charakteryzujących je wzorców. Co więcej, metoda wielokrotnej imputacji jest bardziej wiarygodna niż np. bazowanie na średniej. Pozwala ona zachować naturalny rozkład danych, w tym ich zmienność.

library(mice)
HR_imputed <- mice(HR, m= 5, maxit = 50, seed = 500)
complete_data <- complete(HR_imputed)

Identyfikacja odstających danych

Do identyfikacji i wizualizacji danych odstających (outliers) użyliśmy poniższego kodu. Krok ten służy wykryciu błędów w danych lub nietypowych przypadków, które mogą zaburzyć analizę i doprowadzić nas do błędnych wniosków.

find_outliers <- function(x) {
  Q1 <- quantile(x, 0.25, na.rm = TRUE)
  Q3 <- quantile(x, 0.75, na.rm = TRUE)
  IQR <- Q3 - Q1
  lower_bound <- Q1 - 1.5 * IQR
  upper_bound <- Q3 + 1.5 * IQR
  which(x < lower_bound | x > upper_bound)
}

boxplot(complete_data$YearsAtCompany)

boxplot(complete_data$NumCompaniesWorked)

boxplot(complete_data$PerformanceRating)

boxplot(complete_data$StockOptionLevel)

boxplot(complete_data$TotalWorkingYears)

Analiza opisowa

Do naszej analizy opisowej użyliśmy biblioteki ggplot2. Dzięki niej stworzyliśmy wykres pudełkowy, który wizualizuje rozkład zarobków w zależności od attrition, czyli rotacji pracownika. Wykres pomaga nam zrozumieć, czy istnieje różnica w zarobkach pracowników, którzy opuścili firmę oraz tych, którzy zdecydowali się kontynuować swoją karierę.

library(ggplot2)
library(dplyr)

ggplot(complete_data, aes(x = Attrition, y = MonthlyIncome, fill = Attrition)) +
  geom_boxplot() +
  labs(
    title = "Rozkład miesięcznych dochodów w zależności od rotacji",
    x = "Rotacja (Attrition)",
    y = "Miesięczne dochody (Monthly Income)"
  ) +
  theme_minimal()

Na powyższym wykresie widzimy rozkład miesięcznych zarobków pracowników w zależności od ich rotacji. Mediana dla pracowników, którzy pozostali w firmie jest wyższa niż dla tych, którzy odeszli. Na podstawie naszej analizy możemy stwierdzić, że pracownicy z niższymi zarobkami mogą być bardziej skłonni do odejścia. Może to być sugestią do dalszej analizy oraz zmian w strukturze wynagrodzeń w firmie.

Rozkład dochodu względem rotacji
Zmienna No
N = 1,234
Yes
N = 236
p-value1
Dochód

<0.001
    Mean 6,828 4,832
    SD 4,847 3,664
    Median 5,204 3,271
    Q1 3,195 2,379
    Q3 8,740 5,948
    Min 1,051 1,081
    Max 19,973 19,859
1 Wilcoxon rank sum test

Wizualizacja danych

Mapa korelacji

Wykreślając mapę korelacji tworzymy macierz, która prezentuje relacje między różnymi zmiennymi numerycznymi w zbiorze danych.

Im intensywniejszy kolor, tym silniejsza jest korelacja.

Czerwień wskazuje na dodatnią korelację, niebieskie kolory oznaczają korelację ujemną, a biel/szarość oznacza brak istotnej korelacji między zmiennymi.

W celu stworzenia poniższej mapy, zmieniliśmy zmienne kategoryczne nominalne na zmienne numeryczne.

complete_data$Attrition <- ifelse(complete_data$Attrition == "Yes", 1, 0)
dane_numeryczne <- complete_data[sapply(complete_data, is.numeric)]
korelacje <- cor(dane_numeryczne, use = "complete.obs")

library(ggplot2)
library(reshape2)

mapa_korelacji <- melt(korelacje)
ggplot(data = mapa_korelacji, aes(x = Var1, y = Var2, fill = value)) +
  geom_tile() +
  scale_fill_gradient2(low = "blue", high = "red", mid = "white", midpoint = 0, limit = c(-1,1), space = "Lab", name="Korelacja") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, vjust = 1, size = 8, hjust = 1)) +
  coord_fixed()

library(ggplot2)
complete_data$Attrition <- ifelse(complete_data$Attrition == 1, "Yes", "No")
complete_data$OverTime <- as.factor(complete_data$OverTime)
ggplot(complete_data, aes(x = "", fill = Attrition)) +
  geom_bar(position = "fill") +  
  coord_polar(theta = "y") +  
  labs(
    title = "Procentowy udział rotacji w zależności od nadgodzin",
    fill = "Rotacja (Attrition)" ,
    x = NULL, 
    y = NULL
  ) +
  scale_y_continuous(labels = scales::percent) +  
facet_wrap(~OverTime, labeller = labeller(OverTime = c("No" = "Brak nadgodzin", "Yes" = "Nadgodziny"))) +
  theme_minimal() +
  theme(axis.text.x = element_blank(),  
        axis.text.y = element_blank(), 
        axis.ticks = element_blank(),  
        panel.grid = element_blank())

Na powyższym wykresie kołowym widzimy dane dotyczące wpływu nadgodzin na decyzję o odejściu z pracy. Wyższy odsetek osób opuszczających firmę w grupie osób z nadgodzinami może sugerować, że nadgodziny wpływają negatywnie na satysfakcję z pracy i są jedną z przyczyn jej zmiany.

library(ggplot2)
library(dplyr)

complete_data$JobRole <- as.factor(complete_data$JobRole)
complete_data$Attrition <- as.factor(complete_data$Attrition)

percent_data <- complete_data %>%
  group_by(JobRole, Attrition) %>%
  summarise(count = n(), .groups = 'drop') %>%
  group_by(JobRole) %>%
  mutate(percentage = count / sum(count) * 100)

ggplot(percent_data, aes(x = JobRole, y = percentage, fill = Attrition)) +
  geom_bar(stat = "identity", position = "stack") +
  labs(
    title = "Procentowy udział rotacji wg stanowiska pracy",
    x = "Stanowisko pracy",
    y = "Procent (%)",
    fill = "Rotacja (Attrition)"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 7.5, face = "bold")) + 
  geom_text(aes(label = paste0(round(percentage, 1), "%")),  
            position = position_stack(vjust = 0.5), size = 3)

Z kolei następny wykres - słupkowy, ilustruje procentowy udział rotacji pracowników według stanowiska. Sales Representative (39.8%), Laboratory Technician (24.3%) oraz Human Resources (21.2%) to stanowiska z największą rotacją. Najmniejszą rotację odnotowano na stanowisku Research Directora (3.8%), stanowiskach managerskich (5.9%) oraz wśród Healthcare Representatives (7.6%).

library(ggplot2)
library(dplyr)

complete_data$Gender <- as.factor(complete_data$Gender)
complete_data$Attrition <- as.factor(complete_data$Attrition)

complete_data %>%
  group_by(Gender, Attrition) %>%
   summarise(count = n()) %>%
  mutate(percentage = count / sum(count) * 100) %>%
  ggplot(aes(x = "", y = percentage, fill = Attrition)) +
  geom_bar(stat = "identity", width = 1) +
  coord_polar("y", start = 0) +  
  facet_wrap(~ Gender) +  
  labs(
    title = "Płeć a rotacja",
    x = NULL,
    y = NULL,
    fill = "Rotacja (Attrition)"
  ) +
  theme_minimal() +
  theme(axis.text = element_blank(),
        axis.ticks = element_blank(),
        panel.grid = element_blank()) + 
  geom_text(aes(label = paste0(round(percentage, 1), "%")),  
            position = position_stack(vjust = 0.5)) +
  scale_fill_manual(values = c("Yes" = "#ccffcc", "No" = "#9f5f9f"))  
## `summarise()` has grouped output by 'Gender'. You can override using the
## `.groups` argument.

Powyższe wykresy kołowe prezentują procentowy podział rotacji pracowników ze względu na płeć. Widzimy na nich, że z firmy odchodzi stosunkowo więcej mężczyzn (17%) niż kobiet (14.6%).

library(ggplot2)
library(dplyr)

complete_data$Age <- as.numeric(as.character(complete_data$Age))

complete_data %>%
  group_by(Age, Attrition) %>%
  summarise(count = n(), .groups = "drop") %>%
  group_by(Age) %>%
  mutate(percentage = count / sum(count) * 100) %>%
  ggplot(aes(x = Age, y = percentage, fill = Attrition)) +
  geom_bar(stat = "identity", position = "fill") +
  labs(
    title = "Procentowy rozkład rotacji według wieku",
    x = "Wiek",
    y = "Procent (%)"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust=1, size = 8)) +  
  scale_x_continuous(breaks = seq(min(complete_data$Age), max(complete_data$Age), by = 2)) +
  scale_fill_manual(values = c("Yes" = "#A8A8A8", "No" = "#ffebee"))

Z wykresu można zaobserwować większą tendencję do rotacji pracowników w młodszym wieku. Dodatkowo po 40 roku życia widać niewielką tendencję wzrostową rotacji, wraz ze wzrostem wieku, do około 58 roku życia.

complete_data %>%
  group_by(DistanceFromHome, Attrition) %>%
  summarise(count = n(), .groups = "drop") %>%
  group_by(DistanceFromHome) %>%
  mutate(percentage = count / sum(count) * 100) %>%
  ggplot(aes(x = DistanceFromHome, y = percentage, fill = Attrition)) +
  geom_bar(stat = "identity", position = "fill") +
  labs(
    title = "Procentowy rozkład rotacji według dystansu od miejsca pracy",
    x = "Dystans od miejsca pracy",
    y = "Procent (%)"
  ) +
  theme_minimal() + 
  scale_fill_manual(values = c("Yes" = "#263238", "No" = "#e1bee7"))

Powyższy wykres obrazuje rozkład rotacji według dystansu od miejsca pracy. Większość pracowników nie decyduje się na zmianę pracy ze względu na czas dojazdu, jednak widzimy na wykresie tendencję wzrostową wraz ze wrostem liczby kilometrów oddzielających pracownika od miejsca pracy. Szczególnie zauważalne jest to dla wartości powyżej 8 km.

Wnioskowanie

complete_data$Attrition <- as.factor(complete_data$Attrition)  
anova_result <- aov(MonthlyIncome ~ Attrition, data = complete_data)
summary(anova_result)
##               Df    Sum Sq   Mean Sq F value  Pr(>F)    
## Attrition      1 7.892e+08 789242369   36.06 2.4e-09 ***
## Residuals   1468 3.213e+10  21884527                    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

P-value jest znacznie mniejsze niż 0.05, co oznacza, że różnica dochodów między grupami Attrition (Yes vs. No) jest istotna statystycznie.

Porównanie dokładne parami

library(ggstatsplot)
## You can cite this package as:
##      Patil, I. (2021). Visualizations with statistical details: The 'ggstatsplot' approach.
##      Journal of Open Source Software, 6(61), 3167, doi:10.21105/joss.03167
ggbetweenstats(data=complete_data,x=Attrition,y=MonthlyIncome,type="np",pairwise.display="ns",p.adjust.method="fdr")

Interpretacja wyników: średnie wynagrodzenie w grupie “No” (pracownicy, którzy zostali) jest wyższe (Me = 5204) niż w grupie “Yes” (pracownicy, którzy odeszli) (Me = 3270.50).

W tym teście p-value rownież jest mniejsze niż 0.05, co wskazuje na to, że różnica w wynagrodzeniu między grupami nie jest przypadkowa. Jest statystycznie istotna.

Współczynnik efektu (r-biserial = 0.30) wskazuje na umiarkowaną siłę efektu, co oznacza, że wynagrodzenie ma pewien wpływ na decyzję o odejściu z firmy.

Test t-Studenta

complete_data$Attrition <- as.factor(complete_data$Attrition)
t_test_result <- t.test(MonthlyIncome ~ Attrition, data = complete_data, var.equal = FALSE) 
print(t_test_result)
## 
##  Welch Two Sample t-test
## 
## data:  MonthlyIncome by Attrition
## t = 7.2434, df = 409.89, p-value = 2.184e-12
## alternative hypothesis: true difference in means between group No and group Yes is not equal to 0
## 95 percent confidence interval:
##  1454.278 2537.628
## sample estimates:
##  mean in group No mean in group Yes 
##          6828.271          4832.318

Bardzo mała wartość p-value(< 0.05) Odrzucamy hipotezę zerową H₀ → Wynagrodzenie istotnie różni się między grupami. t = -10.5

Wysoka wartość bezwzględna t oznacza dużą różnicę między grupami. 95% przedział ufności: [-2150.24, -1350.56]

Przedział nie obejmuje zera, więc różnica jest istotna statystycznie. Oznacza to, że średnie wynagrodzenie osób, które odeszły, było od 1350 do 2150 niższe niż tych, którzy zostali.

Podsumowanie

Podsumowanie projektu “Projekt Zespołowy HR” Projekt “Projekt Zespołowy HR” skupił się na analizie czynników wpływających na rotację pracowników (attrition) w organizacji. Wykorzystując zestaw danych dotyczący zatrudnienia, przeprowadziliśmy kompleksowe przetwarzanie danych, analizę opisową oraz wizualizację wyników.

Po przygotowaniu danych do analizy: usunięciu zbędnych kolumn oraz duplikatów, imputacji oraz analizy danych odstających, podsumowaliśmy dane przez analizę opisową. Wykazaliśmy i zwizualizowaliśmy rozkład dochodów pracowników względem rotacji. Następnie dzięki wizualizacji danych przy pomocy wykresów słupkowych, czy kołowych, zauważyliśmy pewne tendencje wpływające na zmienną attrition - między innymi najczęściej rotowane stanowiska czy aspekt wieku, płci i odległości od miejsca pracy.
Dzięki Testom statystycznym(t-studenta, ANOVA) wykazaliśmy istotność statystyczną między wysokością dochodów, a rotacją.

Na podstawie przeprowadzonych analiz możemy stwierdzić, że rotacja pracowników jest zależna od wielu czynników, takich jak zarobki, stanowisko, wiek, płeć oraz odległość do miejsca pracy. Wyniki mogą posłużyć organizacjom do opracowania skuteczniejszych strategii retencji pracowników, np. poprzez dostosowanie polityki wynagrodzeń, oferowanie elastycznych form zatrudnienia czy wprowadzenie programów wsparcia dla pracowników mieszkających daleko od biura.

Projekt dostarcza cennych wskazówek dla zespołów HR i menedżerów, pomagając w optymalizacji zarządzania zasobami ludzkimi oraz redukcji kosztów związanych z rotacją pracowników.