Garments Production¶
This dataset includes important attributes of the garment manufacturing process and the productivity of the employees which had been collected manually and also been validated by the industry experts.
Attribute Information:
- date : Date in MM-DD-YYYY
- day : Day of the Week
- quarter : A portion of the month. A month was divided into four quarters
- department : Associated department with the instance
- team : Associated team number with the instance
- no_of_workers : Number of workers in each team
- no_of_style_change : Number of changes in the style of a particular product
- targeted_productivity : Targeted productivity set by the Authority for each team for each day.
- smv : Standard Minute Value, it is the allocated time for a task
- wip : Work in progress. Includes the number of unfinished items for products
- overtime : Represents the amount of overtime by each team in minutes
- incentive : Represents the amount of financial incentive (in BDT) that enables or motivates a particular course of action.
- idletime : The amount of time when the production was interrupted due to several reasons
- idlemen : The number of workers who were idle due to production interruption
- actual_productivity : The actual % of productivity that was delivered by the workers. It ranges from 0-1.
Load Libraries and Dataset¶
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error
from scipy.stats import pearsonr
df = pd.read_csv('garments_worker_productivity.csv')
print(df.shape)
df.head()
len(df[df['over_time']==0])
EDA¶
Inspect, Clean, and Validate. Show underlying patterns and relationships within datasets.
Dataset general information¶
df.info()
Rename department column wrong typo’s and convert to categorical dtypes¶
df.department.value_counts()
df['department'] = df['department'].replace(['sweing'], 'sewing')
df['department'] = df['department'].replace(['finishing '], 'finishing')
df['department'] = df.department.astype('category')
df.department.value_counts()
Duplicates¶
# variable that store duplicates exactly the same as another row
dups = df.duplicated()
# count duplicates
dups.value_counts()
Theres no duplicates.
Clean the Data (Null Values)¶
Check for the frequency of nulls in all the columns
df.isna().sum()
Theres a large proportion of null values in wip
(42%). The first approach is to investigate how this data was collected or structured.
We need to undestand what type of missing data we are dealing:
- Structurally missing data
- Missing Completely at Random (MCAR)
- Missing at Random (MAR)
- Missing Not at Random (MNAR)
Check the shape of finishing and sewing department seperately.
finishing = df[df['department']=='finishing']
sewing = df[df['department']=='sewing']
print(finishing.shape)
sewing.shape
Check the total number of nulls in wip.
finishing.wip.isna().sum()
Nova! all of the NaN entries are comming from finishing
department. What we need to so now is to ask and verify if there is really no pending works(wip) in the finishing department, just to be sure. Since there is no one to ask for this project I will conclude that this missing data is labeled as Structurally Missing
thus there is no wip in the finishing department.
Now were gonna impute these NaN values. We have a lot of techniques in imputing Nan’s (ex. time series LOCF and NOCB, pairwise, listwise etc.) in this scenario we will convert these NaN in to zero.
# Converting wip NaN values to zero
df.wip.fillna(0, inplace=True)
Visualize the null datapoints¶
We convert first all nulls into 0 before plotting thats because matplot library does’nt read nulls so if we plot our variables without converting, null will not show in the plot.
# Code preparation for plotting.
# Mapping departent to 0 and 1 as integer
df['dept_mapped'] = df.department.map({
'finishing':0,
'sewing':1})
# convert to int dtype
df.dept_mapped = df.dept_mapped.astype('int64')
# plt.figure(figsize=(1, 1))
g = sns.lmplot(x='dept_mapped', y='wip',
data=df,
x_jitter=0.15,
y_jitter=0.15,
fit_reg=False,
scatter_kws={'alpha':0.2})
g.set(xticks=range(2))
g.set_xticklabels(['finishing', 'sewing'])
plt.show()
Convert date to datetime dtype¶
# date format is MM-DD-YYYY
df['date'] = pd.to_datetime(df.date, format="%m/%d/%Y")
Augment data with additional columns(add month and work_week)
# add month
df['month'] = df['date'].dt.month
# Adding `work_week` columns. It's easier to analyze production data per work week.
# create a list of our conditions
conditions = [
#January
(df['month'] == 1) & (df['quarter'] == 'Quarter1'), #ww1
(df['month'] == 1) & (df['quarter'] == 'Quarter2'), #ww2
(df['month'] == 1) & (df['quarter'] == 'Quarter3'), #ww3
(df['month'] == 1) & (df['quarter'] == 'Quarter4'), #ww4
(df['month'] == 1) & (df['quarter'] == 'Quarter5'), #ww4
#February
(df['month'] == 2) & (df['quarter'] == 'Quarter1'), #ww5
(df['month'] == 2) & (df['quarter'] == 'Quarter2'), #ww6
(df['month'] == 2) & (df['quarter'] == 'Quarter3'), #ww7
(df['month'] == 2) & (df['quarter'] == 'Quarter4'), #ww8
(df['month'] == 2) & (df['quarter'] == 'Quarter5'), #ww8
#March
(df['month'] == 3) & (df['quarter'] == 'Quarter1'), #ww9
(df['month'] == 3) & (df['quarter'] == 'Quarter2'), #ww_10
(df['month'] == 3) & (df['quarter'] == 'Quarter3'), #ww_11
(df['month'] == 3) & (df['quarter'] == 'Quarter4'), #ww_12
(df['month'] == 3) & (df['quarter'] == 'Quarter5'), #ww_12
]
values = ['ww1','ww2','ww3','ww4','ww4',
'ww5','ww6','ww7','ww8','ww8',
'ww9','ww_10','ww_11','ww_12','ww_12']
df['work_week'] = np.select(conditions, values)
Quater column to categorical dtypes¶
quarter_order = ['Quarter1', 'Quarter2', 'Quarter3', 'Quarter4', 'Quarter5' ]
df.quarter = pd.Categorical(df.quarter, quarter_order, ordered=True)
# verify quarter order
df.quarter.unique()
Day column to categorical dtypes¶
df.day.unique()
day_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Saturday', 'Sunday' ]
df.day = pd.Categorical(df.day, day_order, ordered=True)
# verify day order
df.day.unique()
Inspect zero value (60% and up)¶
Let’s investigate this zero values.
# how many zeros are there in each columns?
# (df == 0).sum(axis=0) # uncomment to show result
print('Proportion of zeros: ')
(df == 0).sum(axis=0) / len(df) * 100
We have a very large proportion of zero value in wip
, incentive
, idle_time
, idle_men
, and no_of_style_change
.
wip
This 42% of zeros are came from finishing department alternately the remaining 58% are work in progress by the sewing department.incentive
50% or half of the total production days got incentiveidle_time
This large proportion is a good indication that the the production is running smoothly.idle_men
Same as idle_time, most of the teams/workers are working deligently. 98% are zero value.no_of_style_change
87.72% are zero values. Seems fair there is only 12% of change of product style that caused idle_time. Changing style might result to idle_time if not properly executed.
Feature Distribution¶
After doing some cleaning and data wrangling, we can now plot the histogram of the dataset for inspection.
# we will not include date in this visualization
df.drop(columns=['date']).hist(figsize=(10, 7))
plt.tight_layout()
plt.show()
Skewness¶
Skewness is a measure of asymmetry of a distribution.
Important Notes:
- between -0.5 and 0.5, fairly symmetrical
- between -1 and -0.5 or between 0.5 and 1, moderately skewed
- less than -1 or greater than 1, highly skewed
# select numeric columns only
df_numeric = df.select_dtypes(include=np.number).columns.tolist()
for i in df_numeric:
print(i + ' ==> ' + str(round(df[i].skew(),2)))
Kurtosis()¶
kurtosis determines the heaviness of the distribution tails. Determine the volume of the outlier.
- If the distribution is tall and thin it is called a
leptokurtic
distribution(Kurtosis > 3). Values in a leptokurtic distribution are near the mean or at the extremes. - A flat distribution where the values are moderately spread out is called
platykurtic
(Kurtosis <3) distribution. - A distribution whose shape is in between a leptokurtic distribution and a platykurtic distribution is called a
mesokurtic
(Kurtosis=3) distribution. A mesokurtic distribution looks more close to a normal distribution.
‘Note’
- High kurtosis in a df set is an indicator that df has heavy outliers.
- Low kurtosis in a df set is an indicator that df has lack of outliers.
df.select_dtypes(include=np.number).kurt()
Statistical Analysis¶
1. team
¶
Fairly distributed and flat. We will group the dataset by team then get the average productivity.
- Whats the best team so far?
note:
- Productivity is scaled from 0-1(0-100%)
- Mean is more preferable than median if the distribution is fairly symmetrical.
team = df.groupby('team').mean() # groupby then get the mean
team['diff_in_productivity'] = team.actual_productivity - team.targeted_productivity
# limit to 2 decimal places
team = team.round(2)
team[['targeted_productivity', 'actual_productivity', 'diff_in_productivity']]
Top 3 Productive Team
team.actual_productivity.sort_values(ascending=False).head(3)
Visualization
#
x = team.index
y = team.actual_productivity
plt.figure(figsize=(8,5))
ax1 = plt.subplot()
sns.barplot(x=x, y=y, palette = 'deep')
plt.title('Productivity by Team')
for i in ax1.containers:
ax1.bar_label(i,)
plt.tight_layout()
plt.show()
Team productivity by department
team_dept = df.groupby(['team','department']).mean()
team_dept['diff_productivity'] = team_dept.actual_productivity - team_dept.targeted_productivity
team_dept = team_dept.round(2)
# Show first 3 teams
team_dept[['targeted_productivity', 'actual_productivity', 'diff_productivity']].head(6)
Visualization
# code preparation for visualization
team_act_prod = []
for i in team_dept.actual_productivity:
team_act_prod.append(i)
sewing = team_act_prod[1::2]
finishing= team_act_prod[0::2]
# team_dept is a 2d list
# example code for accessing 2d indeces
# team 2 finishing dept would be:
# team_dept.actual_productivity[2][0]
#
fig, ax = plt.subplots(figsize=(12,6))
x = np.arange(1,13)
width = 0.35 # the width of the bars
rects1 = ax.bar(x - width/2, finishing, width, label= 'finishing')
rects2 = ax.bar(x + width/2, sewing, width, label='sewing')
ax.set_xticks(x)
ax.set_xlabel('Teams')
ax.set_ylabel('Actual Productivity')
def autolabel(rects):
"""Attach a text label above each bar in *rects*, displaying its height."""
for rect in rects:
height = rect.get_height()
ax.annotate('{}'.format(height),
xy=(rect.get_x() + rect.get_width() / 2, height),
xytext=(0, 3), # 3 points vertical offset
textcoords="offset points",
ha='center', va='bottom')
plt.title('Actual Productivity by Team Department')
autolabel(rects1)
autolabel(rects2)
plt.tight_layout()
plt.legend(loc='upper center')
plt.show()
Hypothesis¶
Two T-test
Comapare team 1 and 3 since their productivity score are pretty close.
null:
There is no significant difference between team1 and team3 with respect to their productivity.alt
: There is significant difference between team1 and team3.
from scipy.stats import ttest_ind
team1 = df[df.team == 1]
team3 = df[df.team == 3]
plt.hist(team1.actual_productivity, alpha=0.5, label='team1')
plt.hist(team3.actual_productivity,alpha=0.5, label='team3')
plt.legend()
plt.show()
# Check if STD is equal
# a ratio between 0.9 and 1.1 should suffice
ratio = np.std(team1.actual_productivity) / np.std(team3.actual_productivity)
ratio
#run the t-test here:
tstat, pval =ttest_ind(team1.actual_productivity, team3.actual_productivity)
pval
Productivity between team1 and team3 has no significant difference.
2. target productivity
¶
Heavily left skewed. This is great, target productivy should have more high values.
- Predicted vs. Actual Productivity
Note
- We group our dataset by work_week then we get the mean. Anternativily we can use the median if the distribution of particular work_week is in heavily skewed shape. For simplicity we just use the mean. I already check the overall distribution (ww1-ww10) and it’s in fairly bell shaped.
print('Average Productivity per work_week')
target_actual = df.groupby('work_week').mean()
target_actual[['targeted_productivity', 'actual_productivity']]
Visualization
# setting variables
# target productivity average per workweek
tp_avg = df.groupby('work_week')['targeted_productivity'].mean()
# actual productivity average per workweek
ap_avg = df.groupby('work_week')['actual_productivity'].mean()
plt.figure(figsize=(10,5))
plt.plot(tp_avg.index, tp_avg, marker='o')
plt.plot(ap_avg.index, ap_avg, marker='o')
plt.xlabel('work weeks')
plt.ylabel('productivity score')
plt.legend(['targeted productivity', 'actual productivity'])
plt.show()
We have a very good ww4 and ww5 however after ww5 our productivity crashed to a very low level.
# Checking the reason behind the crash
idle = df.groupby('work_week').sum()
idle['idle_time']
We have a major downtime during ww5 that caused the fall of actual productivity.
3. smv
¶
Distribution is faily symmetrical
- Check if theres variables correlated to smv(ex. Is smv correlate to over_time?
def correlation_checker(col):
plt.figure(figsize=(6, 6))
corr_matrix = df.corr()
corr_target = corr_matrix[[col]].drop(labels=[col])
corr_target = corr_target.sort_values(by=[col], ascending=False)
sns.heatmap(corr_target, annot=True, fmt='.3', cmap='RdBu_r')
plt.show()
correlation_checker('smv')
There are three feature’s that has high correlation to smv. However I’m more interested in no_of_workers and over_time, let’s do scatter plot these features againts smv.
df_pairplot = df[['smv', 'no_of_workers', 'over_time' ]]
sns.pairplot(df_pairplot)
plt.show()
As smv(allocated time for a task) increases, the number of workers and overtime also increase. Increasing No_of_workers and over_time with respect to smv is to ensure that production will finish the task on time.
4. wip
¶
Heavily right skewed, as seen in above graph most of the datapoints are at zero and only few has value. Kurtosis at 110 indicating that the distribution is very tall(the mode has a very high proportion).
wip are task that does’nt finish yet, we can view this as delay. Let’s check for what work_week has the most wip.
# sum all wip per work_week
df_wip = df.groupby('work_week').sum()
df_wip['wip']
plt.figure(figsize=(8,5))
ax1 = plt.subplot()
sns.barplot(x= df_wip.index ,
y= df_wip['wip'],
palette = 'deep')
plt.title('Work in Progress')
for i in ax1.containers:
ax1.bar_label(i,)
plt.tight_layout()
plt.show()
We have a large work in progress last ww5. This is expected as we already see that there is a major idle_time(downtime) in ww5. Wonder what really happened in ww5.
5. over_time
¶
Moderately right skewed. Also a good sign, we must lessen OT while maintenaning high productivity.
- How much over_time does production has per work_week? Also the number of workers?
# set variables
ww = df.groupby('work_week').sum() # we use groupby then sum
ww['cummulative'] = ww.over_time.cumsum()
ww[['no_of_workers', 'over_time', 'cummulative']]
- There are 642450 overtime minutes from work week1 with 4305 workers. An average of 2.48 OT hours per worker . (642450 / 4305 we will get 149 minutes then we divide by 60 to get 2.48 hours per worker)
Visualization
#
plt.figure(figsize=(14, 6))
# set variables
x = ww.index
y = ww.over_time
# plot1
# sum of overtime per work_week
ax1 = plt.subplot(1,2,1)
sns.barplot(x = x, y = y, data=df)
ax1.set_xticklabels(ax1.get_xticklabels(),rotation = 80)
plt.title('Overtime per Work Week ')
for i in ax1.containers:
ax1.bar_label(i,)
#plot2
# cumulative sum of work_week
ax2 = plt.subplot(1,2,2)
sns.lineplot(x=x, y=ww.over_time.cumsum(), marker='o', )
plt.title('Cumulative Overtime')
plt.tight_layout()
plt.show()
6. incentive
¶
Heavily right skewed, most observation don’t have incentives. Kurtosis at very high value(299) indicating that there is a large proportion of single observation(the zero value).
- Is this high value incentives has correlation with actual_productivy?
- Take note, we should do an investigation with the outliers.
from scipy.stats import pearsonr
corr, p = pearsonr(df.incentive, df.actual_productivity)
corr, p
plt.figure(figsize=(10, 5))
ax1 = plt.subplot(1,2,1)
ax1 = sns.regplot(x=df.actual_productivity, y=df.incentive, scatter_kws={'alpha':0.3})
# Zoom in
ax2 = plt.subplot(1,2,2)
plt.title('Zoom in')
plt.scatter(df.actual_productivity, df.incentive, alpha=0.3)
plt.ylim(0,200)
plt.xlabel('actual_productivity')
plt.ylabel('incentive')
plt.show()
Pearson score is in nearly zero which mean that there’s no correlation. We have 50% zero value from incentive column that affecting our analyzation.
Most of the incentive that has value are from between 25 and 150, but how did we get a value of around 3500? Thats is for further investigation that we cant’ solve in this given dataset.
Without these zero values and outliers I think we could get a good meaning from this data.¶
Note:
- Large proportion of any values that does’nt have variation does’nt give us insight.
- 50% are zero value from incentive (large proportion of zero values does’nt give much meaning in our analyzation).
- There are 10(1.69%) outliers from total number of incentives that has value
- These outliers happened in just one day.(see below)
Pairwise deletion/repalcement¶
Delete or replace only the column that we are working on. Visualization does not read NaN values, we can now plot again without these zeros.
import warnings
warnings.filterwarnings('ignore')
# replace zero value in incentive w/ np.nan
df.incentive = df['incentive'].replace(0,np.nan)
# drop np.nan values
df_x = df.dropna(subset=['incentive'], axis=0)
# impute outliers with mean value
df_x['incentive'] = df_x['incentive'].where(df_x['incentive'] < 500,
df_x.incentive.mean())
corr, p = pearsonr(df_x.incentive, df_x.actual_productivity)
corr
plt.figure(figsize=(10, 5))
ax1 = plt.subplot(1,2,1)
ax1 = sns.regplot(x=df_x.actual_productivity, y=df_x.incentive, scatter_kws={'alpha':0.3})
# Zoom in
ax2 = plt.subplot(1,2,2)
plt.title('Zoom in')
plt.scatter(df_x.actual_productivity, df_x.incentive, alpha=0.3)
# plt.xlim(0,200)
plt.xlabel('actual_productivity')
plt.ylabel('incentive')
plt.show()
Without the zero values and outliers, we got a meaningful insight.
Correlation is now at .778 suggesting that incentive and actual_productivity has a direct relationship to each other.
Follow up question:
¶
When this extremely high incentive happened?
# check details for this extreme incentives
df[df['incentive']>2500]
This extreme incentives happened on ww_10 particularly in the finishing department
ww10 = df[
(df.work_week == 'ww_10') &
(df.day == 'Monday') &
(df.department == 'finishing')
]
ww10.sort_values(by='team')
Check the count of these extreme incentives if equal to the number of teams who received incentive last ww10 dated 2015-03-09.
len(df[df['incentive']>500])
Choomss!! Our suspesion are correct! is there any special event in this day 2015-03-09, management are giving huge incentives.
7. no_of_workers
¶
Graph looks binomial, is there two group/kind/types of workers?
# import sklean library
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
import warnings
Check what features has strong correlation to no_of_workers.
correlation_checker('no_of_workers')
# subset of our dataset
df_cluster = df[['no_of_workers', 'smv']]
# Normalise our data set
scaler = StandardScaler()
df_cluster = scaler.fit_transform(df_cluster)
Finding the best k
value¶
A good model is one with low inertia and a low number of clusters (K)
# hide/show code
clusters = []
inertias = []
# Checking what is the best 'k' value
# try for 9
for k in range(1,10):
km = KMeans(n_clusters=k, random_state=0)
km.fit(df_cluster)
# append clusters and inertias
inertias.append(km.inertia_)
clusters.append(km)
plt.plot(range(len(inertias)), inertias, '-o')
plt.xlabel('number of clusters (k)')
plt.ylabel('inertia')
plt.show()
Check for silhoutte score¶
for i in range(1,9,1):
print("---------------------------------------")
print(clusters[i])
print("Silhouette score:",silhouette_score(df_cluster, clusters[i].predict(df_cluster)))
2 clusters seem pretty good. Let’s apply clustering using k=2.
Kmeans with 2 clusters¶
km = KMeans(n_clusters=2, random_state=0)
# fit and predict labels
y_km = km.fit_predict(df_cluster)
y_km
Assigning variables¶
# Note:
# first column is the no_of_workers
# 2nd column is the smv
df_cluster[0:5]
no_of_workers = df_cluster[:,0]
smv = df_cluster[:,1]
# first 5 records
print(no_of_workers[0:5])
print(smv[0:5])
Setting up centroids¶
Center points of each clusters.
centers = km.cluster_centers_
centers
Visualized¶
plt.rcParams['axes.facecolor'] = 'gray'
plt.figure(figsize=(8,5))
plt.scatter(no_of_workers, smv, c=y_km, alpha=0.5)
plt.scatter(centers[0][0], centers[0][1], marker='*', color ='purple', s=300, label ='purple centroid (0)', edgecolor = 'black')
plt.scatter(centers[1][0], centers[1][1], marker='*', color ='yellow', s=300, label ='blue centroid (1)', edgecolor = 'black')
plt.xlabel('number of workers')
plt.ylabel('smv(allocated time for a task)')
plt.legend()
plt.title('Scaled clustering')
plt.show()
Scatter plot again but this time we will revert back to un_scaled value
# turn our predicted value first into dataframe
group_worker = pd.DataFrame(y_km)
# concatenate, assign it to a new variable
df_new = pd.concat([df, group_worker], axis=1, join='inner')
# # rename column
df_new = df_new.rename(columns={0: 'group_worker'})
plt.rcParams['axes.facecolor'] = 'gray'
sns.scatterplot(x= 'no_of_workers', y= 'smv', data=df_new, hue='group_worker', palette=['purple','yellow'])
plt.show()
plt.clf
We have two groups to choose from depending on smv value.
- yellow = low number of worker needed
- purple = more worker needed
smv 0 to 10
= (yellow, need up to 30 workers)smv 11 to 50
= (purple, need 30 up to 60 workers)
8. no_of_style_change¶
Does no_of_style_change produce idle_time?
Frequency of unique value in no_of_style_change
df.no_of_style_change.value_counts()
Proportion
import plotly.express as px
fig = px.pie(df, 'no_of_style_change')
fig.update_layout(title="Proportion of no_of_style_change")
fig.update_traces(textposition='inside',
textinfo='percent+label', showlegend=True)
fig.update_traces(pull= 0.02)
fig.show('png')
Correlation between no_of_style_change and idle_time.
from scipy.stats import pearsonr
corr, p = pearsonr(df.no_of_style_change, df.idle_time)
corr, p
There’s no significant correlation between no_of_style_change and idle_time. Also the pval is at very high value.
# visualize
plt.rcParams['axes.facecolor'] = 'white'
# sns.set(rc={'figure.figsize':(8,8)})
ax1 = sns.lmplot(x='no_of_style_change', y='idle_time',
data=df,
x_jitter=0.15,
y_jitter=0.15,
fit_reg=False,
scatter_kws={'alpha':0.5})
ax1.set(xticks=range(3))
ax1.set_xticklabels(df.no_of_style_change.unique())
ax1.set(ylim=(0,10))
# 2nd plot
ax2 = sns.lmplot(x='no_of_style_change', y='idle_time',
data=df,
x_jitter=0.15,
y_jitter=0.15,
fit_reg=False,
scatter_kws={'alpha':0.5})
ax2.set(xticks=range(3))
ax2.set_xticklabels(df.no_of_style_change.unique())
ax2.set(ylim=(0,.25))
plt.title('ZOOM IN')
plt.show()
How many idle_time does the production had?
change_style_idle = df[df['idle_time']>=0]
# we use count because we need to determine how many idle_time does the production had that is >= 2
change_style_idle.groupby('no_of_style_change').count()[['idle_time']]
There are 114 for one change in style and 33 count for changing style two times.
- Changing style can cause idle time up from 15 seconds up to 8 minutes.
10. Prodcutivity per month¶
jan = df[df['month']==1]
feb = df[df['month']==2]
mar = df[df['month']==3]
plt.hist(jan.actual_productivity, alpha=.5, label='January')
plt.hist(feb.actual_productivity, alpha=.5, label='February')
plt.hist(mar.actual_productivity, alpha=.5, label='March')
plt.xlabel('Actual Productivity')
plt.ylabel('Count')
plt.legend()
plt.show()
We have a very good productivy last January.
Note
: There are records of productivy higher than 1. (we assumed that productivity are scaled from 0-1)
feb_higher_prod = feb[feb.actual_productivity >1][['actual_productivity']]
print(len(feb_higher_prod))
feb_higher_prod.sort_values(by='actual_productivity', ascending=False).head(3)
There are 28 entries of productivity score higher that our intended max value(1). Is this a typo error or system error? We will leave this question for now.
Hypothesis Test¶
Anova and Turkey¶
Test the average productivity per month.
Null:
All Months productivy are the sameAlt:
Months productivity are not thesame
df['month_name'] = df.month.map({
1:'January',
2:'February',
3:'March'
})
from scipy.stats import f_oneway
from statsmodels.stats.multicomp import pairwise_tukeyhsd
fstat, pval = f_oneway(jan.actual_productivity,
feb.actual_productivity,
mar.actual_productivity)
pval
tukey_results = pairwise_tukeyhsd(df.actual_productivity, df.month_name, 0.05)
print(tukey_results)
sns.boxplot(x='month_name', y='actual_productivity', data=df )
plt.show()
- January and February has significant difference in productivity
- February and March has no significant difference
- January and March has significant difference
Outliers¶
#define functions
def showoutliers(df, column_name = ""):
iqr = df[column_name].quantile(.75) - df[column_name].quantile(.25)
# lower whisker
lowerbound = (df[column_name].quantile(.25)) - iqr * 1.5
# upper whisker
upperbound = (df[column_name].quantile(.75)) + iqr * 1.5
# dfpoints beyond lower whisker
lowerbound_outliers = df[df[column_name] < lowerbound]
# adtapoint beyond upper whisker
higherbound_outliers = df[df[column_name] > upperbound]
# outliers
outliers = pd.concat([lowerbound_outliers,higherbound_outliers])
return outliers
def countoutliers(df, column_name = ""):
iqr = df[column_name].quantile(.75) - df[column_name].quantile(.25)
lowerbound = (df[column_name].quantile(.25)) - iqr * 1.5
upperbound = (df[column_name].quantile(.75)) + iqr * 1.5
lowerbound_outliers = df[df[column_name] < lowerbound]
higherbound_outliers = df[df[column_name] > upperbound]
outliers = pd.concat([lowerbound_outliers,higherbound_outliers])
count = len(outliers)
return {column_name : count}
def Replace_Outliers(df_name, value, column_name = ""):
iqr = df_name[column_name].quantile(.75) - df_name[column_name].quantile(.25)
lowerbound = (df_name[column_name].quantile(.25)) - iqr * 1.5
upperbound = (df_name[column_name].quantile(.75)) + iqr * 1.5
df_name[column_name] = np.where(df_name[column_name] > upperbound, value, df_name[column_name])
df_name[column_name] = np.where(df_name[column_name] < lowerbound, value, df_name[column_name])
Create a dataset with only numeric values
# selected features only
df_n = df[[ 'targeted_productivity', 'smv', 'wip', 'over_time', 'incentive', 'no_of_workers', 'actual_productivity']]
# df_n
Count outliers
column_list = df_n.columns
column_list = np.array(column_list)
for i in df_n:
print (countoutliers(df_n, i))
Proportion outliers
for i in column_list:
col = i
perc = countoutliers(df_n, i)[i] / len(df_n)
print (col + ': ' + str('{:.2f}'.format(perc*100)) + '%')
df_n.plot(kind='box',
subplots=True,
sharey=False,
figsize=(20, 7))
# increase spacing between subplots
plt.subplots_adjust(wspace=0.5)
plt.tight_layout()
plt.show()
There’s one columns that cought my attention. The ‘Incentive’, it has the most skewed and has high kurtosis values. Let’s focus on these and do some imputations.
Replace outliers with 75 percentile of the distribution
df_new.incentive.describe()
Replace_Outliers(df_new, 50, 'incentive')
# visualize
plt.figure(figsize=(4,3))
sns.boxplot(x='incentive', data=df_new)
plt.show()
To be continue…¶