Exploratory & Inferential Analysis of IT Network Infrastructure Incidents

Data Analytics II — Capstone Case Study 1

Author

Muhammad O. Musa - 2025-MMBA-8-042

Published

May 26, 2026

1 Executive Summary

This study applies five core techniques from exploratory and inferential analytics to a real-world dataset of 146 closed IT incident tickets extracted from the SysAid IT Service Management (ITSM) platform at IHS Towers. The dataset spans March 2025 to April 2026 and covers incidents managed by the Global IT Network Infrastructure team across multiple regions.

The analysis begins with Exploratory Data Analysis (EDA) to profile the dataset, detecting a pronounced right skew in resolution windows (mean = 217 hours vs. median = 74 hours) and identifying 12 statistical outliers. Data visualisation reveals that Connectivity Services dominates the incident portfolio (56%) and that ticket volumes peak during weekday business hours. Hypothesis testing using the Kruskal-Wallis and Chi-squared tests finds no statistically significant differences in resolution windows across urgency levels (p = 0.251) or in incident category distribution across urgency tiers (p = 0.518). Spearman correlation analysis uncovers a near-perfect association between urgency and priority classifications (\(\rho\) = 0.883, p < 0.001), suggesting redundancy. Multiple linear regression confirms that ticket metadata fields collectively explain less than 4% of variation in resolution time (R² = 0.04), indicating that resolution timelines are driven by external, unrecorded factors. These findings carry direct implications for SLA policy design, incident triage workflows, and data-capture strategy.

2 Professional Disclosure

I currently work as the Manager, Global IT Network Infrastructure at IHS Towers, a multinational telecommunications infrastructure company operating across multiple countries. My role involves managing global network infrastructure operations, enterprise connectivity, incident management governance, IT service delivery, escalation coordination, and operational performance monitoring.

The Global IT Network Infrastructure function depends heavily on efficient incident management processes to maintain service availability, minimise operational downtime, and ensure SLA compliance across geographically distributed operations.

The dataset used in this analysis was extracted from the organisation’s SysAid IT Service Management (ITSM) platform and anonymised to protect employee and operational privacy.

2.1 Relevance of Techniques to My Role

As Manager of Global IT Network Infrastructure at IHS Towers, I oversee the resolution of network-related incidents across multiple countries and regional Network Operations Centres (NOCs). My team handles connectivity outages, VPN failures, application downtimes, and workspace enablement issues reported through SysAid.

Each of the five analytical techniques in Case Study 1 maps directly to operational decisions I make:

  • EDA is operationally relevant because it helps identify unusual incident patterns, outliers, recurring operational bottlenecks, and data quality concerns within IT operations. As a network infrastructure manager, understanding ticket distributions and operational anomalies supports proactive service improvement.
  • Visualisation is essential for communicating operational performance trends to executive management, regional IT teams, and service governance stakeholders. Visual dashboards simplify interpretation of SLA performance and operational workload distribution.
  • Hypothesis testing supports evidence-based operational decision-making. It enables management to determine whether observed differences in incident resolution performance across regions, priorities, or categories are statistically significant rather than random operational variation. It also allows me to rigorously examine whether the urgency classifications assigned to tickets genuinely correspond to different resolution outcomes — a question that directly affects SLA policy.
  • Correlation analysis helps identify redundant classification fields and reveals which factors move together, informing decisions about which variables to track.
  • Regression modelling provides the analytical foundation for predicting resolution timelines and identifying the most impactful drivers of delay — critical for capacity planning and resource allocation.

3 Data Collection & Sampling

3.1 Data Source

The primary dataset was extracted from SysAid, the IT Service Management (ITSM) tool used by IHS Towers for logging, tracking, and managing IT service requests and incidents. SysAid serves as the single system of record for all IT incidents across the organisation’s global operations.

3.2 Extraction Method

The data was obtained through SysAid’s built-in reporting module:

  1. Navigated to Service Desk > Service Records > Analytics/Reports
  2. Applied filters:
    • Date range: March 2025 – April 2026
    • Status: Closed
    • SR Type: Incident
    • Category: Information Technology
  3. Exported as CSV with all available fields

3.3 Sampling Approach

A census approach was adopted — all 146 closed IT incidents matching the filter criteria were included, rather than drawing a sample. This eliminates sampling bias and ensures the analysis reflects the complete operational reality of the period under review.

3.4 Data Provenance

Attribute Detail
Source system SysAid ITSM (Cloud instance)
Data owner IT Service Management Team, IHS Towers
Extraction date May 2026
Time period covered 29 March 2025 – 29 April 2026 (13 months)
Population All closed IT incidents in the period
Observations 146
Raw variables 13
File format CSV (Exported_Service_Records.csv)

3.5 Anonymisation

All personally identifiable information (PII) was anonymised prior to analysis. Specifically, the Request user field was replaced with pseudonymised names. Technician identifiers in the Assigned to field were retained in their system username format (e.g., law.odi) as these are not considered PII in isolation and are necessary for workload analysis.

3.6 Ethical Considerations

The data represents operational service records, not personal or sensitive data about individuals. The analysis was conducted in accordance with IHS Towers’ data governance policies. No external or third-party data was used.

4 Data Description

This section loads the raw SysAid export, engineers additional features required for analysis, and profiles the resulting dataset.

4.1 Data Loading and Feature Engineering

Code
# ---- Load required libraries ----
library(tidyverse)    # Data manipulation and visualisation
library(lubridate)    # Date-time parsing
library(knitr)        # Table formatting
library(kableExtra)   # Enhanced table styling
library(scales)       # Formatting scales
Code
# ---- Load raw data from SysAid CSV export ----
df <- read_csv("Exported_Service_Records.csv", show_col_types = FALSE)

# ---- Parse date-time columns ----
# SysAid exports dates in M/D/YYYY H:MM format
df <- df %>%
  mutate(
    request_time = mdy_hm(`Request time`),
    due_date     = mdy_hm(`Due date`)
  )

# ---- Feature Engineering ----
df <- df %>%
  mutate(
    # Primary outcome: resolution window in hours
    resolution_window_hrs = as.numeric(
      difftime(due_date, request_time, units = "hours")
    ),

    # Temporal features extracted from request timestamp
    hour_created  = hour(request_time),
    day_of_week   = wday(request_time, label = TRUE, abbr = FALSE),
    month_created = floor_date(request_time, "month"),
    week_created  = floor_date(request_time, "week"),

    # Business hours flag: weekday 08:00-17:59
    is_weekday = ifelse(wday(request_time) %in% 2:6, 1, 0),
    is_business_hours = ifelse(
      hour_created >= 8 & hour_created <= 17 & is_weekday == 1, 1, 0
    ),

    # Numeric encoding for ordinal variables
    urgency_numeric = case_when(
      Urgency == "Low"    ~ 1,
      Urgency == "Medium" ~ 2,
      Urgency == "High"   ~ 3
    ),
    priority_numeric = case_when(
      Priority == "Minor"    ~ 1,
      Priority == "Moderate" ~ 2,
      Priority == "Normal"   ~ 3,
      Priority == "Major"    ~ 4
    ),

    # Grouped sub-category (merge small groups into "Other")
    subcat_grouped = case_when(
      `Sub-category` %in% c(
        "Voice and Data Communication",
        "IT Security",
        "Datacentre & Systems Infrastructure"
      ) ~ "Other",
      TRUE ~ `Sub-category`
    ),

    # Log-transformed resolution window for regression
    log_resolution_hrs = log1p(resolution_window_hrs),

    # Extract technician username from domain\\username format
    technician = str_remove(`Assigned to`, ".*\\\\")
  )

# ---- Display dataset overview ----
cat("Dataset dimensions:", nrow(df), "rows x", ncol(df), "columns\n")
Dataset dimensions: 146 rows x 27 columns
Code
cat("Date range:", as.character(min(df$request_time)),
    "to", as.character(max(df$request_time)), "\n")
Date range: 2025-03-29 04:04:00 to 2026-04-29 10:52:00 
Code
# ---- Load required libraries ----
import pandas as pd
import numpy as np

# ---- Load raw data from SysAid CSV export ----
df_py = pd.read_csv("Exported_Service_Records.csv")

# ---- Parse date-time columns ----
df_py['request_time'] = pd.to_datetime(df_py['Request time'], format='mixed')
df_py['due_date']     = pd.to_datetime(df_py['Due date'], format='mixed')

# ---- Feature Engineering ----
# Primary outcome: resolution window in hours
df_py['resolution_window_hrs'] = (
    (df_py['due_date'] - df_py['request_time']).dt.total_seconds() / 3600
)

# Temporal features
df_py['hour_created'] = df_py['request_time'].dt.hour
df_py['day_of_week']  = df_py['request_time'].dt.day_name()
df_py['is_weekday']   = (df_py['request_time'].dt.dayofweek < 5).astype(int)

# Business hours flag: weekday 08:00-17:59
df_py['is_business_hours'] = (
    (df_py['hour_created'] >= 8) &
    (df_py['hour_created'] <= 17) &
    (df_py['is_weekday'] == 1)
).astype(int)

# Numeric encoding for ordinal variables
urgency_map  = {'Low': 1, 'Medium': 2, 'High': 3}
priority_map = {'Minor': 1, 'Moderate': 2, 'Normal': 3, 'Major': 4}
df_py['urgency_numeric']  = df_py['Urgency'].map(urgency_map)
df_py['priority_numeric'] = df_py['Priority'].map(priority_map)

# Grouped sub-category
df_py['subcat_grouped'] = df_py['Sub-category'].replace({
    'Voice and Data Communication': 'Other',
    'IT Security': 'Other',
    'Datacentre & Systems Infrastructure': 'Other'
})

# Log-transformed resolution window
df_py['log_resolution_hrs'] = np.log1p(df_py['resolution_window_hrs'])

# Extract technician username
df_py['technician'] = df_py['Assigned to'].str.split('\\').str[-1]

# ---- Display dataset overview ----
print(f"Dataset dimensions: {df_py.shape[0]} rows x {df_py.shape[1]} columns")
Dataset dimensions: 146 rows x 25 columns
Code
print(f"Date range: {df_py['request_time'].min()} to {df_py['request_time'].max()}")
Date range: 2025-03-29 04:04:00 to 2026-04-29 10:52:00

4.2 Variable Profile

The table below describes each variable used in the analysis, including both the raw SysAid fields and the engineered features.

Variable Type Description Source
ID Identifier Unique SysAid ticket number Raw
Urgency Categorical (3 levels) Low, Medium, High Raw
Priority Categorical (4 levels) Minor, Moderate, Normal, Major Raw
Sub-category Categorical (6 levels) Service domain of the incident Raw
3rd level category Categorical (24 levels) Granular issue classification Raw
Assigned to Categorical Technician assigned to the ticket Raw
request_time DateTime Timestamp when ticket was created Parsed
due_date DateTime Timestamp for target resolution Parsed
resolution_window_hrs Numeric (continuous) Hours between creation and due date — primary outcome Engineered
hour_created Numeric (discrete, 0–23) Hour of day the ticket was created Engineered
day_of_week Categorical (7 levels) Day of week the ticket was created Engineered
is_business_hours Binary (0/1) 1 if created weekday 08:00–17:59 Engineered
urgency_numeric Ordinal numeric (1–3) Numeric encoding of Urgency Engineered
priority_numeric Ordinal numeric (1–4) Numeric encoding of Priority Engineered
subcat_grouped Categorical (4 levels) Sub-category with small groups merged into “Other” Engineered
log_resolution_hrs Numeric (continuous) log(1 + resolution_window_hrs) for regression Engineered

Variable requirements check (per Case Study 1 brief):

  • Numeric variables (≥3): resolution_window_hrs, hour_created, urgency_numeric, priority_numeric, is_business_hours, log_resolution_hrs
  • Categorical variables (≥2): Urgency, Priority, Sub-category, day_of_week, subcat_grouped
  • Date/time variable (≥1): request_time, due_date
  • Observations (≥100): 146 ✓

4.3 Summary Statistics

Code
# ---- Summary statistics for resolution window ----
df %>%
  summarise(
    n          = n(),
    mean_hrs   = round(mean(resolution_window_hrs), 2),
    median_hrs = round(median(resolution_window_hrs), 2),
    sd_hrs     = round(sd(resolution_window_hrs), 2),
    min_hrs    = round(min(resolution_window_hrs), 2),
    max_hrs    = round(max(resolution_window_hrs), 2),
    Q1         = round(quantile(resolution_window_hrs, 0.25), 2),
    Q3         = round(quantile(resolution_window_hrs, 0.75), 2)
  ) %>%
  kable(caption = "Summary Statistics: Resolution Window (Hours)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Summary Statistics: Resolution Window (Hours)
n mean_hrs median_hrs sd_hrs min_hrs max_hrs Q1 Q3
146 217.09 74.23 356.97 12.5 2970.75 49 240
Code
# ---- Summary statistics for resolution window ----
print("Summary Statistics: Resolution Window (Hours)")
Summary Statistics: Resolution Window (Hours)
Code
print(df_py['resolution_window_hrs'].describe().round(2))
count     146.00
mean      217.09
std       356.97
min        12.50
25%        49.00
50%        74.23
75%       240.00
max      2970.75
Name: resolution_window_hrs, dtype: float64

5 Technique 1: Exploratory Data Analysis (EDA)

5.1 Theory Recap

Exploratory Data Analysis (EDA) is a foundational analytical approach popularised by Tukey (1977) that emphasises understanding the structure, patterns, and anomalies in data before applying formal statistical tests. EDA encompasses summary statistics, distributional profiling, missing-value diagnostics, and outlier detection (Wickham & Grolemund, 2017). It is an essential precursor to inferential analysis because violations of assumptions — such as non-normality or the presence of extreme outliers — can invalidate downstream tests.

5.2 Business Justification

Before making any claims about what drives incident resolution timelines, I need to understand the basic shape and quality of my data. EDA reveals whether the SysAid data is complete, whether resolution windows are normally distributed (affecting test selection), and whether extreme outliers exist that could distort averages. For an infrastructure manager, this is the equivalent of “walking the floor” before making operational decisions — understanding the terrain before acting.

5.3 Analysis

Code
# ---- Missing Value Analysis ----
cat("=== Missing Value Check ===\n")
=== Missing Value Check ===
Code
missing <- colSums(is.na(df[, c("ID", "Urgency", "Priority", "Sub-category",
                                  "request_time", "due_date",
                                  "resolution_window_hrs",
                                  "hour_created", "is_business_hours")]))
print(missing)
                   ID               Urgency              Priority 
                    0                     0                     0 
         Sub-category          request_time              due_date 
                    0                     0                     0 
resolution_window_hrs          hour_created     is_business_hours 
                    0                     0                     0 
Code
cat("\nResult: Zero missing values across all analytical variables.\n")

Result: Zero missing values across all analytical variables.
Code
# ---- Distribution Diagnostics ----
cat("=== Distribution Diagnostics ===\n")
=== Distribution Diagnostics ===
Code
# Skewness and kurtosis (using e1071 package)
cat("Skewness:", round(e1071::skewness(df$resolution_window_hrs), 4), "\n")
Skewness: 4.7662 
Code
cat("Kurtosis:", round(e1071::kurtosis(df$resolution_window_hrs), 4), "\n")
Kurtosis: 28.5247 
Code
# Shapiro-Wilk normality test
shapiro_test <- shapiro.test(df$resolution_window_hrs)
cat("Shapiro-Wilk W =", round(shapiro_test$statistic, 6),
    ", p-value =", format.pval(shapiro_test$p.value, digits = 4), "\n")
Shapiro-Wilk W = 0.492431 , p-value = < 2.2e-16 
Code
cat("\nConclusion: Data significantly departs from normality (p < 0.001).\n")

Conclusion: Data significantly departs from normality (p < 0.001).
Code
cat("This justifies the use of non-parametric tests in subsequent sections.\n")
This justifies the use of non-parametric tests in subsequent sections.
Code
# ---- Outlier Detection (IQR Method) ----
Q1 <- quantile(df$resolution_window_hrs, 0.25)
Q3 <- quantile(df$resolution_window_hrs, 0.75)
IQR_val <- Q3 - Q1
lower_fence <- Q1 - 1.5 * IQR_val
upper_fence <- Q3 + 1.5 * IQR_val

outliers <- df %>%
  filter(resolution_window_hrs < lower_fence |
         resolution_window_hrs > upper_fence)

cat("=== Outlier Detection (IQR Method) ===\n")
=== Outlier Detection (IQR Method) ===
Code
cat("Q1:", Q1, " | Q3:", Q3, " | IQR:", IQR_val, "\n")
Q1: 49  | Q3: 240  | IQR: 191 
Code
cat("Lower fence:", lower_fence, " | Upper fence:", upper_fence, "\n")
Lower fence: -237.5  | Upper fence: 526.5 
Code
cat("Number of outliers:", nrow(outliers), "of", nrow(df), "observations\n")
Number of outliers: 12 of 146 observations
Code
# Display outlier records
outliers %>%
  select(ID, Urgency, Priority, `Sub-category`, resolution_window_hrs) %>%
  arrange(desc(resolution_window_hrs)) %>%
  kable(caption = "Outlier Incidents (Beyond 1.5 x IQR)",
        digits = 1) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Outlier Incidents (Beyond 1.5 x IQR)
ID Urgency Priority Sub-category resolution_window_hrs
444553 Low Minor IT Security 2970.8
437340 High Normal Connectivity Services 2116.2
460778 Low Minor Connectivity Services 1458.5
471259 Low Minor Connectivity Services 1221.8
466463 Low Minor Voice and Data Communication 972.1
470826 Low Minor Business Applications 829.3
464601 High Normal Workspace Enablement 750.3
464863 Low Minor Connectivity Services 736.8
441968 Low Minor Business Applications 731.1
477930 Low Minor Connectivity Services 627.5
464316 Low Minor Connectivity Services 623.2
458510 High Normal Connectivity Services 591.0
Code
# ---- Frequency Table: Urgency ----
df %>%
  count(Urgency) %>%
  mutate(pct = round(n / sum(n) * 100, 1)) %>%
  kable(caption = "Incident Count by Urgency Level") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Incident Count by Urgency Level
Urgency n pct
High 27 18.5
Low 102 69.9
Medium 17 11.6
Code
# ---- Frequency Table: Sub-category ----
df %>%
  count(`Sub-category`) %>%
  mutate(pct = round(n / sum(n) * 100, 1)) %>%
  arrange(desc(n)) %>%
  kable(caption = "Incident Count by Sub-category") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Incident Count by Sub-category
Sub-category n pct
Connectivity Services 82 56.2
Business Applications 34 23.3
Workspace Enablement 16 11.0
Voice and Data Communication 6 4.1
IT Security 5 3.4
Datacentre & Systems Infrastructure 3 2.1
Code
from scipy import stats as sp_stats

# ---- Missing Value Analysis ----
key_cols = ['Urgency', 'Priority', 'Sub-category', 'request_time',
            'due_date', 'resolution_window_hrs', 'hour_created',
            'is_business_hours']
print("=== Missing Value Check ===")
=== Missing Value Check ===
Code
print(df_py[key_cols].isnull().sum())
Urgency                  0
Priority                 0
Sub-category             0
request_time             0
due_date                 0
resolution_window_hrs    0
hour_created             0
is_business_hours        0
dtype: int64
Code
print("\nResult: Zero missing values across all analytical variables.")

Result: Zero missing values across all analytical variables.
Code
# ---- Distribution Diagnostics ----
rw = df_py['resolution_window_hrs']
print("=== Distribution Diagnostics ===")
=== Distribution Diagnostics ===
Code
print(f"Skewness: {rw.skew():.4f}")
Skewness: 4.8658
Code
print(f"Kurtosis: {rw.kurtosis():.4f}")
Kurtosis: 30.0201
Code
sw_stat, sw_p = sp_stats.shapiro(rw)
print(f"Shapiro-Wilk W = {sw_stat:.6f}, p-value = {sw_p:.4e}")
Shapiro-Wilk W = 0.492431, p-value = 2.1402e-20
Code
print("\nConclusion: Data significantly departs from normality.")

Conclusion: Data significantly departs from normality.
Code
# ---- Outlier Detection (IQR Method) ----
Q1 = rw.quantile(0.25)
Q3 = rw.quantile(0.75)
IQR_val = Q3 - Q1
lower_fence = Q1 - 1.5 * IQR_val
upper_fence = Q3 + 1.5 * IQR_val
outliers_py = df_py[(rw < lower_fence) | (rw > upper_fence)]

print("=== Outlier Detection (IQR Method) ===")
=== Outlier Detection (IQR Method) ===
Code
print(f"Q1: {Q1:.2f} | Q3: {Q3:.2f} | IQR: {IQR_val:.2f}")
Q1: 49.00 | Q3: 240.00 | IQR: 191.00
Code
print(f"Lower fence: {lower_fence:.2f} | Upper fence: {upper_fence:.2f}")
Lower fence: -237.50 | Upper fence: 526.50
Code
print(f"Number of outliers: {len(outliers_py)} of {len(df_py)} observations")
Number of outliers: 12 of 146 observations
Code
# ---- Frequency Tables ----
print("=== Urgency Distribution ===")
=== Urgency Distribution ===
Code
urg_counts = df_py['Urgency'].value_counts()
urg_pct = (urg_counts / len(df_py) * 100).round(1)
print(pd.DataFrame({'Count': urg_counts, 'Pct': urg_pct}))
         Count   Pct
Urgency             
Low        102  69.9
High        27  18.5
Medium      17  11.6
Code
print("\n=== Sub-category Distribution ===")

=== Sub-category Distribution ===
Code
sub_counts = df_py['Sub-category'].value_counts()
sub_pct = (sub_counts / len(df_py) * 100).round(1)
print(pd.DataFrame({'Count': sub_counts, 'Pct': sub_pct}))
                                     Count   Pct
Sub-category                                    
Connectivity Services                   82  56.2
Business Applications                   34  23.3
Workspace Enablement                    16  11.0
Voice and Data Communication             6   4.1
IT Security                              5   3.4
Datacentre & Systems Infrastructure      3   2.1

5.4 Interpretation

The EDA reveals several important characteristics of the incident data with direct operational implications:

  1. Data completeness: The SysAid export contains zero missing values across all 146 observations and all analytical variables. This is a strength that eliminates the need for imputation and ensures all observations contribute to every analysis. In the context of ITIL best practices for IT Service Management, this level of data completeness indicates mature ticket hygiene — a prerequisite for any data-driven improvement initiative.

  2. Distribution shape: The resolution window is heavily right-skewed (skewness > 2), with a mean of approximately 217 hours but a median of only 74 hours. This three-fold difference between mean and median is a classic signature of IT incident data (Tukey, 1977) and carries a concrete operational implication: any SLA target set using the mean will be breached by the majority of tickets that are actually resolved faster, while simultaneously under-flagging the long-tail incidents that consume disproportionate resources. The Shapiro-Wilk test confirms significant departure from normality (p < 0.001), which justifies the use of non-parametric tests (Kruskal-Wallis, Spearman) in subsequent sections. For management reporting, I will recommend percentile-based metrics (P50, P80, P95) in the Integrated Findings.

  3. Outliers: The IQR method identifies 12 outlier incidents (8.2% of all tickets) with resolution windows exceeding approximately 527 hours (~22 days). While these 12 tickets represent fewer than 1 in 10 incidents, they embody the Pareto principle in incident management — a small fraction of tickets likely consumes a disproportionate share of engineering hours, management attention, and customer frustration. Their root causes may include vendor dependencies (e.g., waiting for ISP circuit restoration), hardware procurement lead times, or cross-regional escalations requiring coordination across time zones. These outliers are retained in the analysis (rather than removed) because they represent real operational outcomes that my team must manage, but they motivate the use of robust methods and log-transformation in the regression section. Operationally, each of these 12 tickets warrants a post-incident review (PIR) to identify systemic blockers.

  4. Category dominance: Connectivity Services accounts for 56% of all incidents, followed by Business Applications (23%) and Workspace Enablement (11%). This concentration directly reflects IHS Towers’ core business — telecommunications infrastructure — where network connectivity is mission-critical. The implication for my team is clear: automation and self-healing investments should be prioritised for Connectivity Services, where even a 10% reduction in ticket volume would eliminate approximately 8 incidents per year, freeing engineering capacity for proactive infrastructure improvements rather than reactive firefighting.

  5. Distinction between resolution window and MTTR: It is important to note that the resolution_window_hrs variable represents the SLA target window (Due date minus Request time), not the actual Mean Time to Resolve (MTTR). This distinction matters operationally: a ticket with a 49-hour SLA window may have been resolved in 2 hours or may have breached at 50 hours — we cannot distinguish these cases from the current data export. Capturing actual resolution timestamps in future exports would unlock true MTTR analysis, which is the gold standard for ITIL Continual Service Improvement (CSI).

6 Technique 2: Data Visualisation

6.1 Theory Recap

Data visualisation translates numerical summaries into graphical representations that reveal patterns, trends, and outliers more intuitively than tables alone (Wickham, 2016). Effective visualisation adheres to principles of clarity, honesty, and efficiency — encoding data in position, length, and colour rather than area or volume where possible (Tufte, 2001). In an operational context, visualisations serve as communication tools for stakeholders who may not engage with statistical output directly.

6.2 Business Justification

As a manager reporting to senior IT leadership, I regularly need to communicate incident trends, workload distribution, and service patterns in formats that drive action. Visualisation answers questions such as: “When do incidents peak?”, “Which service domains create the most load?”, and “Do different urgency levels show visually different resolution patterns?” These graphics form the basis of monthly operational dashboards and capacity planning discussions.

6.3 Analysis

Code
# ---- Plot 1: Histogram of Resolution Window ----
ggplot(df, aes(x = resolution_window_hrs)) +
  geom_histogram(binwidth = 24, fill = "#2C6FAC",
                 colour = "white", alpha = 0.85) +
  geom_vline(aes(xintercept = median(resolution_window_hrs)),
             colour = "red", linetype = "dashed", linewidth = 1) +
  annotate("text",
           x = median(df$resolution_window_hrs) + 50,
           y = Inf,
           label = paste0("Median = ",
                          round(median(df$resolution_window_hrs), 1),
                          " hrs"),
           vjust = 2, colour = "red", fontface = "bold") +
  labs(title = "Distribution of Incident Resolution Window",
       subtitle = "Red dashed line = median; bin width = 24 hours (1 day)",
       x = "Resolution Window (Hours)",
       y = "Frequency") +
  theme_minimal(base_size = 13)

Distribution of Resolution Window (Hours)
Code
# ---- Plot 2: Box Plot by Urgency ----
ggplot(df, aes(x = factor(Urgency, levels = c("Low", "Medium", "High")),
               y = resolution_window_hrs,
               fill = Urgency)) +
  geom_boxplot(alpha = 0.8,
               outlier.colour = "red", outlier.shape = 16) +
  scale_fill_manual(values = c("Low"    = "#4DAF4A",
                                "Medium" = "#FF7F00",
                                "High"   = "#E41A1C")) +
  labs(title = "Resolution Window by Urgency Level",
       subtitle = "Outliers shown as red dots",
       x = "Urgency",
       y = "Resolution Window (Hours)") +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none")

Resolution Window by Urgency Level
Code
# ---- Plot 3: Bar Chart by Sub-category ----
df %>%
  count(`Sub-category`) %>%
  mutate(`Sub-category` = fct_reorder(`Sub-category`, n)) %>%
  ggplot(aes(x = `Sub-category`, y = n, fill = `Sub-category`)) +
  geom_col(alpha = 0.85) +
  geom_text(aes(label = n), hjust = -0.2, fontface = "bold") +
  coord_flip() +
  labs(title = "Incident Volume by Service Sub-category",
       x = NULL,
       y = "Number of Incidents") +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none") +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15)))

Incident Volume by Sub-category
Code
# ---- Plot 4: Weekly Time Series ----
df %>%
  count(week_created) %>%
  ggplot(aes(x = week_created, y = n)) +
  geom_line(colour = "#2C6FAC", linewidth = 0.8) +
  geom_point(colour = "#2C6FAC", size = 2) +
  geom_smooth(method = "loess", se = TRUE,
              colour = "red", linetype = "dashed", alpha = 0.2) +
  labs(title = "Weekly Incident Volume Over Time",
       subtitle = "Red dashed line = LOESS smoothed trend",
       x = "Week",
       y = "Number of Incidents") +
  theme_minimal(base_size = 13)

Weekly Incident Volume Over Time
Code
# ---- Plot 5: Day x Hour Heatmap ----
heatmap_data <- df %>%
  mutate(day_of_week = factor(
    day_of_week,
    levels = c("Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday", "Sunday")
  )) %>%
  count(day_of_week, hour_created)

ggplot(heatmap_data, aes(x = hour_created,
                          y = day_of_week,
                          fill = n)) +
  geom_tile(colour = "white") +
  scale_fill_gradient(low = "#FFF7BC", high = "#D95F0E",
                      name = "Incidents") +
  scale_x_continuous(breaks = seq(0, 23, 2)) +
  labs(title = "Incident Heatmap: When Are Tickets Created?",
       subtitle = "Darker cells indicate higher incident volumes",
       x = "Hour of Day (24h)",
       y = NULL) +
  theme_minimal(base_size = 13)

Incident Heatmap: Day of Week x Hour of Day
Code
# ---- Plot 6: Technician Workload ----
df %>%
  count(technician) %>%
  mutate(technician = fct_reorder(technician, n)) %>%
  ggplot(aes(x = technician, y = n, fill = technician)) +
  geom_col(alpha = 0.85) +
  geom_text(aes(label = n), hjust = -0.2, fontface = "bold") +
  coord_flip() +
  labs(title = "Incident Workload by Technician",
       x = NULL,
       y = "Number of Incidents Assigned") +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none") +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15)))

Workload Distribution Across Technicians
Code
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

fig, axes = plt.subplots(3, 2, figsize=(14, 20))
fig.suptitle("IT Infrastructure Incident Analysis — Visual Summary",
             fontsize=16, fontweight='bold', y=1.01)

# ---- Plot 1: Histogram ----
ax = axes[0, 0]
ax.hist(df_py['resolution_window_hrs'], bins=30,
        color='#2C6FAC', edgecolor='white', alpha=0.85)
median_val = df_py['resolution_window_hrs'].median()
ax.axvline(median_val, color='red', linestyle='--', linewidth=2)
ax.set_title('Distribution of Resolution Window')
ax.set_xlabel('Resolution Window (Hours)')
ax.set_ylabel('Frequency')
ax.annotate(f'Median = {median_val:.1f}h',
            xy=(median_val, 0),
            xytext=(median_val + 100, ax.get_ylim()[1] * 0.8),
            color='red', fontweight='bold',
            arrowprops=dict(arrowstyle='->', color='red'))

# ---- Plot 2: Box Plot by Urgency ----
ax = axes[0, 1]
order = ['Low', 'Medium', 'High']
colors = {'Low': '#4DAF4A', 'Medium': '#FF7F00', 'High': '#E41A1C'}
sns.boxplot(data=df_py, x='Urgency', y='resolution_window_hrs',
            order=order, palette=colors, ax=ax)
ax.set_title('Resolution Window by Urgency Level')
ax.set_ylabel('Resolution Window (Hours)')

# ---- Plot 3: Bar Chart by Sub-category ----
ax = axes[1, 0]
subcat_counts = df_py['Sub-category'].value_counts().sort_values()
subcat_counts.plot.barh(ax=ax, color='#2C6FAC', alpha=0.85)
ax.set_title('Incident Volume by Sub-category')
ax.set_xlabel('Number of Incidents')
for i, v in enumerate(subcat_counts.values):
    ax.text(v + 0.5, i, str(v), va='center', fontweight='bold')

# ---- Plot 4: Weekly Time Series ----
ax = axes[1, 1]
weekly = df_py.set_index('request_time').resample('W').size()
ax.plot(weekly.index, weekly.values,
        color='#2C6FAC', marker='o', markersize=4)
ax.set_title('Weekly Incident Volume Over Time')
ax.set_xlabel('Week')
ax.set_ylabel('Number of Incidents')
ax.tick_params(axis='x', rotation=45)

# ---- Plot 5: Day x Hour Heatmap ----
ax = axes[2, 0]
day_order = ['Monday', 'Tuesday', 'Wednesday',
             'Thursday', 'Friday', 'Saturday', 'Sunday']
heatmap_df = (df_py.groupby(['day_of_week', 'hour_created'])
              .size().unstack(fill_value=0))
heatmap_df = heatmap_df.reindex(day_order)
sns.heatmap(heatmap_df, cmap='YlOrBr', ax=ax,
            cbar_kws={'label': 'Incidents'})
ax.set_title('Incident Heatmap: Day x Hour')
ax.set_xlabel('Hour of Day')
ax.set_ylabel('')

# ---- Plot 6: Technician Workload ----
ax = axes[2, 1]
tech_counts = (df_py['Assigned to'].str.split('\\\\').str[-1]
               .value_counts().sort_values())
tech_counts.plot.barh(ax=ax, color='#2C6FAC', alpha=0.85)
ax.set_title('Incident Workload by Technician')
ax.set_xlabel('Number of Incidents')
for i, v in enumerate(tech_counts.values):
    ax.text(v + 0.3, i, str(v), va='center', fontweight='bold')

plt.tight_layout()
plt.show()

6.4 Interpretation

The visualisations surface several operationally significant patterns that translate directly into management actions:

  1. Right-skewed resolution distribution (Plot 1): The histogram confirms the EDA finding — most incidents cluster within 2–3 days (median ≈ 74 hours), but a long right tail extends to nearly 3,000 hours. This “tale of two populations” is characteristic of IT incident data worldwide and suggests two distinct resolution pathways: a fast track (standard fixes, known-error workarounds, password resets) and a slow track (vendor escalations, hardware replacements, design-level failures). For my team, this implies that a single SLA target is inappropriate — we need tiered SLA targets that differentiate between these populations, perhaps based on initial diagnostic category.

  2. Urgency levels show similar medians (Plot 2): Visually, the three urgency levels have surprisingly similar median resolution windows. High-urgency tickets actually show greater variability (wider box and longer whiskers), not shorter resolution times. This counterintuitive finding suggests that high-urgency tickets may involve more complex problems (e.g., multi-site outages, backbone failures) whose severity prolongs — rather than accelerates — resolution. This challenges the implicit assumption in our triage process that labelling a ticket “High” urgency will make it resolve faster. The formal statistical test in Section 7 will determine whether this visual pattern is statistically significant.

  3. Connectivity Services dominates (Plot 3): With 82 incidents (56%), Connectivity Services generates more than double the volume of Business Applications (34 incidents, 23%). This concentration has direct resource allocation implications: more than half of my team’s reactive workload is consumed by connectivity issues. In ITIL terms, this signals an opportunity for a “shift-left” strategy — investing in network monitoring automation, self-healing scripts, and proactive alerting to intercept connectivity issues before users report them as incidents.

  4. No clear trend in weekly volume (Plot 4): Incident volume fluctuates week to week without a clear upward or downward trend, though the LOESS smoother suggests slightly elevated volumes in early 2026. The absence of a downward trend is itself informative: it means that our current improvement initiatives have not yet reduced the incident arrival rate, reinforcing the need for the structural interventions recommended in this analysis. The weekly variability (ranging from 0 to ~8 incidents per week) also affects capacity planning — my team must be staffed for peak weeks, not average weeks.

  5. Business-hours concentration (Plot 5): The heatmap reveals a clear concentration of incidents during weekday business hours (08:00–18:00, Monday–Friday), with minimal weekend and night-time ticket creation. This pattern validates the current staffing model where full team coverage is provided during business hours, but it also reveals an opportunity: after-hours incidents, though fewer, may take longer to resolve due to reduced staffing. This observation will be examined through the is_business_hours variable in the correlation and regression analyses.

  6. Uneven workload distribution (Plot 6): The technician workload chart reveals significant imbalance — the top two technicians handle approximately 52% of all incidents between them, while other team members handle substantially fewer. This concentration creates operational risk on multiple fronts: burnout and retention risk for overloaded technicians, knowledge concentration (if these individuals leave, institutional knowledge leaves with them), and development deprivation for under-assigned team members who lack exposure to diverse incident types. The recommendation is to implement a round-robin or load-balanced assignment algorithm in SysAid, supplemented by cross-training programmes to ensure all team members can handle the full spectrum of incident categories.

7 Technique 3: Hypothesis Testing

7.1 Theory Recap

Hypothesis testing provides a formal decision-making framework for evaluating claims about population parameters using sample data (Adi, 2024). The procedure involves stating null and alternative hypotheses, selecting a significance level (\(\alpha\)), computing a test statistic, and comparing the resulting p-value to \(\alpha\). When data violate normality assumptions — as established in our EDA — non-parametric alternatives such as the Kruskal-Wallis test (the non-parametric equivalent of one-way ANOVA) and the Chi-squared test of independence offer robust alternatives (Field et al., 2012).

7.2 Business Justification

The urgency classification in SysAid (Low, Medium, High) is intended to signal the severity and expected response speed for each incident. If urgency genuinely drives resolution outcomes, then it validates the triage system. If not, it raises a fundamental question about whether the current classification scheme adds operational value — which directly informs process redesign and SLA policy decisions.

7.3 Hypothesis 1: Resolution Window Across Urgency Levels

Research question: Does the resolution window differ significantly across urgency levels?

  • \(H_0\): The median resolution window is equal across all urgency levels (Low, Medium, High).
  • \(H_1\): At least one urgency level has a significantly different median resolution window.
  • \(\alpha = 0.05\)
  • Test: Kruskal-Wallis rank-sum test (chosen because the resolution window is non-normal, as confirmed by Shapiro-Wilk p < 0.001).
Code
# ---- Assumption Check: Normality per Group ----
cat("=== Shapiro-Wilk Normality Test by Urgency Group ===\n")
=== Shapiro-Wilk Normality Test by Urgency Group ===
Code
for (u in c("Low", "Medium", "High")) {
  subset_data <- df %>% filter(Urgency == u) %>% pull(resolution_window_hrs)
  sw <- shapiro.test(subset_data)
  cat(u, ": W =", round(sw$statistic, 4),
      ", p =", format.pval(sw$p.value, digits = 3), "\n")
}
Low : W = 0.4795 , p = <2e-16 
Medium : W = 0.8057 , p = 0.00243 
High : W = 0.5415 , p = 4.57e-08 
Code
cat("\nAll groups violate normality (p < 0.05).\n")

All groups violate normality (p < 0.05).
Code
cat("Non-parametric Kruskal-Wallis test is justified.\n")
Non-parametric Kruskal-Wallis test is justified.
Code
# ---- Descriptive Statistics by Urgency ----
df %>%
  group_by(Urgency) %>%
  summarise(
    n      = n(),
    mean   = round(mean(resolution_window_hrs), 1),
    median = round(median(resolution_window_hrs), 1),
    sd     = round(sd(resolution_window_hrs), 1),
    .groups = "drop"
  ) %>%
  kable(caption = "Resolution Window Statistics by Urgency Level") %>%
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = FALSE)
Resolution Window Statistics by Urgency Level
Urgency n mean median sd
High 27 261.1 171.0 412.0
Low 102 221.9 73.5 367.7
Medium 17 118.2 73.5 108.5
Code
# ---- Kruskal-Wallis Test ----
kw_test <- kruskal.test(resolution_window_hrs ~ Urgency, data = df)

cat("=== Kruskal-Wallis Test ===\n")
=== Kruskal-Wallis Test ===
Code
cat("H-statistic:", round(kw_test$statistic, 4), "\n")
H-statistic: 2.7636 
Code
cat("Degrees of freedom:", kw_test$parameter, "\n")
Degrees of freedom: 2 
Code
cat("p-value:", round(kw_test$p.value, 4), "\n")
p-value: 0.2511 
Code
# ---- Effect Size: Epsilon-squared ----
n_total <- nrow(df)
k <- length(unique(df$Urgency))
epsilon_sq <- (kw_test$statistic - k + 1) / (n_total - k)

cat("\nEpsilon-squared (effect size):", round(epsilon_sq, 4), "\n")

Epsilon-squared (effect size): 0.0053 
Code
cat("Benchmarks: < 0.01 negligible, 0.01-0.06 small,",
    "0.06-0.14 medium, > 0.14 large\n")
Benchmarks: < 0.01 negligible, 0.01-0.06 small, 0.06-0.14 medium, > 0.14 large
Code
# ---- Decision ----
if (kw_test$p.value > 0.05) {
  cat("\nDECISION: Fail to reject H0.\n")
  cat("No significant difference in resolution window",
      "across urgency levels.\n")
} else {
  cat("\nDECISION: Reject H0. Significant difference detected.\n")
}

DECISION: Fail to reject H0.
No significant difference in resolution window across urgency levels.
Code
# ---- Descriptive Statistics by Urgency ----
print("=== Group Descriptives ===")
=== Group Descriptives ===
Code
for u in ['Low', 'Medium', 'High']:
    subset = df_py[df_py['Urgency'] == u]['resolution_window_hrs']
    print(f"  {u}: n={len(subset)}, mean={subset.mean():.1f}, "
          f"median={subset.median():.1f}, sd={subset.std():.1f}")
  Low: n=102, mean=221.9, median=73.5, sd=367.7
  Medium: n=17, mean=118.2, median=73.5, sd=108.5
  High: n=27, mean=261.1, median=171.0, sd=412.0
Code
# ---- Kruskal-Wallis Test ----
groups = [df_py[df_py['Urgency'] == u]['resolution_window_hrs']
          for u in ['Low', 'Medium', 'High']]
kw_stat, kw_p = sp_stats.kruskal(*groups)

print("=== Kruskal-Wallis Test ===")
=== Kruskal-Wallis Test ===
Code
print(f"H-statistic: {kw_stat:.4f}")
H-statistic: 2.7636
Code
print(f"p-value: {kw_p:.4f}")
p-value: 0.2511
Code
# ---- Effect Size: Epsilon-squared ----
n_total = len(df_py)
k = df_py['Urgency'].nunique()
epsilon_sq = (kw_stat - k + 1) / (n_total - k)
print(f"\nEpsilon-squared: {epsilon_sq:.4f}")

Epsilon-squared: 0.0053
Code
if kw_p > 0.05:
    print("\nDECISION: Fail to reject H0. No significant difference.")
else:
    print("\nDECISION: Reject H0. Significant difference detected.")

DECISION: Fail to reject H0. No significant difference.

7.3.1 Interpretation — Hypothesis 1

The Kruskal-Wallis test yields H = 2.76, p = 0.251, with an epsilon-squared effect size near zero. We fail to reject the null hypothesis at the 5% significance level. In plain language:

There is no statistically significant difference in incident resolution windows across Low, Medium, and High urgency levels.

This is arguably the most critical business finding of the entire analysis. In ITIL v4’s incident management framework, urgency is one of two dimensions (alongside impact) that should determine priority and, consequently, response and resolution targets. Our data shows that this mechanism is not functioning as designed — the urgency tag is being applied but is not translating into differentiated resolution speed.

This disconnect can arise from several root causes:

  • No differentiated SLA targets: If all urgency levels share the same resolution window in SysAid’s SLA configuration, then the classification is cosmetic rather than operational.
  • Technician behaviour is not urgency-driven: Engineers may process tickets in FIFO (first-in, first-out) order regardless of urgency, or they may prioritise based on personal judgment rather than the formal classification.
  • External factors dominate: For network infrastructure incidents, resolution speed is often gated by vendor response times (ISP circuit restoration), hardware procurement cycles, or approval workflows — none of which are accelerated by an urgency label alone.

Operational recommendation: The IT Service Management team should (a) configure differentiated SLA timers in SysAid for each urgency level (e.g., High = 4 hours response / 24 hours resolution; Low = 8 hours / 72 hours), (b) implement automated escalation triggers when High-urgency tickets approach their SLA threshold, and (c) track urgency-level compliance in monthly operational dashboards to create accountability.

7.4 Hypothesis 2: Service Category Distribution Across Urgency Levels

Research question: Is the distribution of incident sub-categories independent of the urgency level assigned?

  • \(H_0\): Sub-category and Urgency are independent (the proportion of incidents in each sub-category is the same across urgency levels).
  • \(H_1\): Sub-category and Urgency are not independent (certain categories are disproportionately associated with higher urgency).
  • \(\alpha = 0.05\)
  • Test: Chi-squared test of independence (with Monte Carlo simulation in R to handle cells with small expected counts).
Code
# ---- Contingency Table ----
ct <- table(df$`Sub-category`, df$Urgency)
cat("=== Contingency Table ===\n")
=== Contingency Table ===
Code
print(ct)
                                     
                                      High Low Medium
  Business Applications                  9  22      3
  Connectivity Services                 14  55     13
  Datacentre & Systems Infrastructure    0   2      1
  IT Security                            1   4      0
  Voice and Data Communication           1   5      0
  Workspace Enablement                   2  14      0
Code
# ---- Expected Counts Check ----
cat("\n=== Expected Counts ===\n")

=== Expected Counts ===
Code
chi_test <- chisq.test(ct, simulate.p.value = TRUE, B = 10000)
print(round(chi_test$expected, 1))
                                     
                                      High  Low Medium
  Business Applications                6.3 23.8    4.0
  Connectivity Services               15.2 57.3    9.5
  Datacentre & Systems Infrastructure  0.6  2.1    0.3
  IT Security                          0.9  3.5    0.6
  Voice and Data Communication         1.1  4.2    0.7
  Workspace Enablement                 3.0 11.2    1.9
Code
cat("\nNote: Some expected counts < 5,",
    "hence Monte Carlo simulation is used.\n")

Note: Some expected counts < 5, hence Monte Carlo simulation is used.
Code
# ---- Chi-squared Test (Monte Carlo) ----
cat("=== Chi-squared Test (Monte Carlo, B = 10,000) ===\n")
=== Chi-squared Test (Monte Carlo, B = 10,000) ===
Code
cat("Chi-squared statistic:", round(chi_test$statistic, 4), "\n")
Chi-squared statistic: 9.1451 
Code
cat("p-value:", round(chi_test$p.value, 4), "\n")
p-value: 0.5126 
Code
# ---- Effect Size: Cramer's V ----
n <- sum(ct)
k <- min(nrow(ct), ncol(ct))
cramers_v <- sqrt(chi_test$statistic / (n * (k - 1)))
cat("\nCramer's V:", round(cramers_v, 4), "\n")

Cramer's V: 0.177 
Code
cat("Benchmarks: < 0.1 negligible, 0.1-0.3 small,",
    "0.3-0.5 medium, > 0.5 large\n")
Benchmarks: < 0.1 negligible, 0.1-0.3 small, 0.3-0.5 medium, > 0.5 large
Code
# ---- Decision ----
if (chi_test$p.value > 0.05) {
  cat("\nDECISION: Fail to reject H0.\n")
  cat("Sub-category and Urgency are independent.\n")
} else {
  cat("\nDECISION: Reject H0. Significant association detected.\n")
}

DECISION: Fail to reject H0.
Sub-category and Urgency are independent.
Code
# ---- Contingency Table ----
ct_py = pd.crosstab(df_py['Sub-category'], df_py['Urgency'])
print("=== Contingency Table ===")
=== Contingency Table ===
Code
print(ct_py)
Urgency                              High  Low  Medium
Sub-category                                          
Business Applications                   9   22       3
Connectivity Services                  14   55      13
Datacentre & Systems Infrastructure     0    2       1
IT Security                             1    4       0
Voice and Data Communication            1    5       0
Workspace Enablement                    2   14       0
Code
# ---- Chi-squared Test ----
chi2, chi_p, dof, expected = sp_stats.chi2_contingency(ct_py)
print(f"\n=== Chi-squared Test ===")

=== Chi-squared Test ===
Code
print(f"Chi-squared: {chi2:.4f}")
Chi-squared: 9.1451
Code
print(f"p-value: {chi_p:.4f}")
p-value: 0.5184
Code
print(f"Degrees of freedom: {dof}")
Degrees of freedom: 10
Code
# ---- Expected Counts ----
print(f"\n=== Expected Counts ===")

=== Expected Counts ===
Code
print(pd.DataFrame(expected.round(1),
                    index=ct_py.index, columns=ct_py.columns))
Urgency                              High   Low  Medium
Sub-category                                           
Business Applications                 6.3  23.8     4.0
Connectivity Services                15.2  57.3     9.5
Datacentre & Systems Infrastructure   0.6   2.1     0.3
IT Security                           0.9   3.5     0.6
Voice and Data Communication          1.1   4.2     0.7
Workspace Enablement                  3.0  11.2     1.9
Code
# ---- Cramer's V ----
n = ct_py.sum().sum()
k = min(ct_py.shape)
cramers_v = np.sqrt(chi2 / (n * (k - 1)))
print(f"\nCramer's V: {cramers_v:.4f}")

Cramer's V: 0.1770
Code
if chi_p > 0.05:
    print("\nDECISION: Fail to reject H0. Variables are independent.")
else:
    print("\nDECISION: Reject H0. Significant association detected.")

DECISION: Fail to reject H0. Variables are independent.

7.4.1 Interpretation — Hypothesis 2

The Chi-squared test yields \(\chi^2\) = 9.15, p = 0.518, with a Cramer’s V indicating negligible-to-small effect size. We fail to reject the null hypothesis.

The distribution of incident sub-categories does not differ significantly across urgency levels.

This means that no particular service domain (e.g., Connectivity Services, Business Applications) is disproportionately associated with higher-urgency classifications. Urgency appears to be assigned somewhat uniformly across incident types.

This finding has two important implications for my team’s triage process:

  1. Triage consistency: The uniform distribution of urgency across categories suggests that triage agents are evaluating urgency based on business impact and context (e.g., number of affected users, time sensitivity) rather than reflexively assigning high urgency to certain categories. This is actually aligned with ITIL best practice, where urgency should reflect “how quickly the business needs a resolution” independently of the technical domain. However, this should be formalised in a triage decision matrix rather than left to individual judgment, to ensure consistency when different analysts handle similar tickets.

  2. Training implication: Since urgency assignment is category-independent, any triage training programme should focus on impact assessment skills (e.g., “How many users are affected?”, “Is this a revenue-impacting system?”) rather than category-specific rules. This is a cost-effective training approach because it applies universally across all incident types.

8 Technique 4: Correlation Analysis

8.1 Theory Recap

Correlation analysis measures the strength and direction of monotonic relationships between pairs of variables (Adi, 2024). The Spearman rank correlation coefficient (\(\rho\)) is preferred over Pearson’s \(r\) when data are ordinal or violate normality assumptions — both conditions present in our dataset (Field et al., 2012). Spearman’s \(\rho\) ranges from -1 (perfect negative monotonic relationship) to +1 (perfect positive), with 0 indicating no monotonic association.

8.2 Business Justification

Understanding which variables move together informs several operational decisions: Are urgency and priority classifications redundant? Does the time of day a ticket is created correlate with how long it takes to resolve? Are tickets created outside business hours associated with longer resolution windows? These questions directly affect classification scheme design, staffing schedules, and SLA targets.

8.3 Analysis

Code
library(ggcorrplot)

# ---- Select numeric variables for correlation ----
numeric_vars <- df %>%
  select(resolution_window_hrs, urgency_numeric,
         priority_numeric, hour_created, is_business_hours)

# ---- Compute Spearman Correlation Matrix ----
corr_matrix <- cor(numeric_vars, method = "spearman",
                   use = "complete.obs")

cat("=== Spearman Correlation Matrix ===\n")
=== Spearman Correlation Matrix ===
Code
print(round(corr_matrix, 4))
                      resolution_window_hrs urgency_numeric priority_numeric
resolution_window_hrs                1.0000         -0.0379          -0.0640
urgency_numeric                     -0.0379          1.0000           0.8827
priority_numeric                    -0.0640          0.8827           1.0000
hour_created                         0.1076         -0.0852          -0.1325
is_business_hours                   -0.1258         -0.1392          -0.1231
                      hour_created is_business_hours
resolution_window_hrs       0.1076           -0.1258
urgency_numeric            -0.0852           -0.1392
priority_numeric           -0.1325           -0.1231
hour_created                1.0000           -0.0030
is_business_hours          -0.0030            1.0000
Code
# ---- Correlation Heatmap ----
ggcorrplot(corr_matrix,
           method = "square",
           type = "lower",
           lab = TRUE,
           lab_size = 4,
           colors = c("#E41A1C", "white", "#2C6FAC"),
           title = "Spearman Rank Correlation Matrix",
           ggtheme = theme_minimal(base_size = 13))

Spearman Correlation Matrix — Numeric Variables
Code
# ---- Statistical Significance of Each Pair ----
cat("=== Spearman Correlation p-values ===\n\n")
=== Spearman Correlation p-values ===
Code
vars <- c("resolution_window_hrs", "urgency_numeric",
           "priority_numeric", "hour_created", "is_business_hours")

for (i in 1:(length(vars) - 1)) {
  for (j in (i + 1):length(vars)) {
    test <- cor.test(numeric_vars[[vars[i]]],
                     numeric_vars[[vars[j]]],
                     method = "spearman")
    cat(vars[i], "vs", vars[j],
        ": rho =", round(test$estimate, 4),
        ", p =", format.pval(test$p.value, digits = 4), "\n")
  }
}
resolution_window_hrs vs urgency_numeric : rho = -0.0379 , p = 0.6496 
resolution_window_hrs vs priority_numeric : rho = -0.064 , p = 0.443 
resolution_window_hrs vs hour_created : rho = 0.1076 , p = 0.1963 
resolution_window_hrs vs is_business_hours : rho = -0.1258 , p = 0.1302 
urgency_numeric vs priority_numeric : rho = 0.8827 , p = < 2.2e-16 
urgency_numeric vs hour_created : rho = -0.0852 , p = 0.3065 
urgency_numeric vs is_business_hours : rho = -0.1392 , p = 0.09372 
priority_numeric vs hour_created : rho = -0.1325 , p = 0.111 
priority_numeric vs is_business_hours : rho = -0.1231 , p = 0.1389 
hour_created vs is_business_hours : rho = -0.003 , p = 0.9717 
Code
import matplotlib.pyplot as plt
import seaborn as sns

# ---- Select numeric variables ----
numeric_cols = ['resolution_window_hrs', 'urgency_numeric',
                'priority_numeric', 'hour_created', 'is_business_hours']
corr_matrix_py = df_py[numeric_cols].corr(method='spearman')

print("=== Spearman Correlation Matrix ===")
=== Spearman Correlation Matrix ===
Code
print(corr_matrix_py.round(4))
                       resolution_window_hrs  ...  is_business_hours
resolution_window_hrs                 1.0000  ...            -0.1258
urgency_numeric                      -0.0379  ...            -0.1392
priority_numeric                     -0.0640  ...            -0.1231
hour_created                          0.1076  ...            -0.0030
is_business_hours                    -0.1258  ...             1.0000

[5 rows x 5 columns]
Code
# ---- Heatmap (lower triangle only) ----
fig, ax = plt.subplots(figsize=(8, 6))
mask = np.triu(np.ones_like(corr_matrix_py, dtype=bool))
sns.heatmap(corr_matrix_py, mask=mask, annot=True, fmt='.3f',
            cmap='RdBu_r', center=0, vmin=-1, vmax=1,
            square=True, linewidths=1, ax=ax)
ax.set_title('Spearman Rank Correlation Matrix',
             fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

Code
# ---- Pairwise Spearman p-values ----
print("=== Pairwise Spearman p-values ===")
=== Pairwise Spearman p-values ===
Code
for i in range(len(numeric_cols)):
    for j in range(i + 1, len(numeric_cols)):
        rho, p = sp_stats.spearmanr(
            df_py[numeric_cols[i]], df_py[numeric_cols[j]]
        )
        print(f"  {numeric_cols[i]} vs {numeric_cols[j]}: "
              f"rho={rho:.4f}, p={p:.4f}")
  resolution_window_hrs vs urgency_numeric: rho=-0.0379, p=0.6496
  resolution_window_hrs vs priority_numeric: rho=-0.0640, p=0.4430
  resolution_window_hrs vs hour_created: rho=0.1076, p=0.1963
  resolution_window_hrs vs is_business_hours: rho=-0.1258, p=0.1302
  urgency_numeric vs priority_numeric: rho=0.8827, p=0.0000
  urgency_numeric vs hour_created: rho=-0.0852, p=0.3065
  urgency_numeric vs is_business_hours: rho=-0.1392, p=0.0937
  priority_numeric vs hour_created: rho=-0.1325, p=0.1110
  priority_numeric vs is_business_hours: rho=-0.1231, p=0.1389
  hour_created vs is_business_hours: rho=-0.0030, p=0.9717

8.4 Interpretation

8.4.1 1. Urgency and Priority are near-redundant (\(\rho\) = 0.883, p < 0.001)

The strongest correlation in the matrix by far is between urgency and priority classifications. A \(\rho\) of 0.883 indicates a very strong positive monotonic relationship — when urgency goes up, priority almost always goes up too. This near-redundancy suggests that maintaining both fields in the ITSM system adds classification overhead without providing distinct information.

In ITIL v4’s framework, urgency and priority are meant to serve distinct purposes: urgency captures “how quickly does the business need this resolved?” while priority is a composite of urgency × impact that determines the order of resolution. In practice at IHS Towers, our data shows these two fields are being treated as interchangeable — a ticket marked “High urgency” is almost always also marked “High priority” (and vice versa), meaning the impact dimension is not being independently assessed.

Concrete ITSM configuration recommendation:

  • Option A — Consolidate: Remove the separate urgency field from SysAid’s ticket form and rely solely on a 4-level priority field (Critical/High/Medium/Low) with clear definitions tied to business impact and user count. This reduces triage time and eliminates confusion.
  • Option B — Enforce independence: If both fields are retained, implement a priority matrix in SysAid where priority is auto-calculated from urgency × impact (e.g., High urgency + High impact = Critical priority; High urgency + Low impact = Medium priority). This creates the separation that ITIL intends and would produce a much lower correlation between the two fields — which would also improve the regression model in Section 9 by eliminating multicollinearity.

Either option improves data quality for future analytics and reduces the cognitive burden on triage agents by an estimated 10–15 seconds per ticket — a small saving that compounds across 146+ incidents per year.

8.4.2 2. No variable is strongly correlated with resolution window

The correlations between resolution window and all other numeric variables are weak (\(|\rho| < 0.13\)) and statistically non-significant (p > 0.05). This means:

  • Resolution time is not meaningfully associated with urgency (\(\rho\) = -0.038, p = 0.650) — confirming the hypothesis test finding.
  • Business-hours tickets show a very slight tendency toward shorter resolution (\(\rho\) = -0.126, p = 0.130) — suggestive but not statistically significant. This makes intuitive sense: tickets created during business hours have immediate access to the support team.
  • Hour of creation shows a weak positive trend (\(\rho\) = 0.108, p = 0.196) — tickets created later in the day may slightly extend into the next working day.

8.4.3 3. Implications for regression modelling

The weak correlations between predictors and the outcome variable foreshadow that the regression model (Section 9) will have limited explanatory power. This is itself an important finding: resolution time in IT infrastructure is likely driven by exogenous, unrecorded factors (vendor response times, spare parts availability, cross-team escalations) rather than by ticket-level metadata.

9 Technique 5: Regression Analysis

9.1 Theory Recap

Multiple linear regression models the relationship between a continuous dependent variable and two or more independent variables (Adi, 2024). The Ordinary Least Squares (OLS) method minimises the sum of squared residuals to estimate coefficients, each of which represents the expected change in the outcome per unit change in the predictor, holding all other predictors constant (James et al., 2021). Key assumptions include linearity, homoscedasticity, normality of residuals, and absence of multicollinearity. When the dependent variable is right-skewed, a log transformation — specifically \(\log(1 + y)\) — is commonly applied to better approximate the normality assumption (Field et al., 2012).

9.2 Business Justification

The ultimate operational question for my team is: Can we predict how long an incident will take to resolve based on the information available at the time of ticket creation? If ticket metadata (urgency, priority, category, time of creation) are meaningful predictors, this enables proactive SLA management — we could flag tickets at risk of breaching targets before they do. If these predictors are weak, it tells us that resolution timelines depend on factors we are not currently capturing in SysAid, which itself is an actionable insight for data-capture improvement.

9.3 Model Specification

\[ \log(1 + \text{resolution\_window\_hrs}) = \beta_0 + \beta_1 \cdot \text{urgency\_numeric} + \beta_2 \cdot \text{priority\_numeric} + \beta_3 \cdot \text{is\_business\_hours} + \beta_4 \cdot \text{subcat\_grouped} + \epsilon \]

Dependent variable: log_resolution_hrs (log-transformed resolution window)

Independent variables:

  • urgency_numeric (ordinal: Low=1, Medium=2, High=3)
  • priority_numeric (ordinal: Minor=1, Moderate=2, Normal=3, Major=4)
  • is_business_hours (binary: 1 = weekday 08:00–17:59)
  • subcat_grouped (categorical: Connectivity Services, Business Applications, Workspace Enablement, Other)

9.4 Analysis

Code
library(car)   # For VIF
library(broom) # For tidy model output

# ---- Fit OLS Model ----
model <- lm(log_resolution_hrs ~ urgency_numeric + priority_numeric +
              is_business_hours + subcat_grouped,
            data = df)

# ---- Model Summary ----
cat("=== OLS Regression Summary ===\n")
=== OLS Regression Summary ===
Code
summary(model)

Call:
lm(formula = log_resolution_hrs ~ urgency_numeric + priority_numeric + 
    is_business_hours + subcat_grouped, data = df)

Residuals:
    Min      1Q  Median      3Q     Max 
-2.0276 -0.7313 -0.3284  0.8370  2.7104 

Coefficients:
                                    Estimate Std. Error t value Pr(>|t|)    
(Intercept)                          4.99757    0.27513  18.164   <2e-16 ***
urgency_numeric                      0.03200    0.24894   0.129   0.8979    
priority_numeric                    -0.07705    0.23052  -0.334   0.7387    
is_business_hours                   -0.39423    0.19637  -2.008   0.0466 *  
subcat_groupedConnectivity Services  0.08506    0.22123   0.384   0.7012    
subcat_groupedOther                  0.34283    0.33052   1.037   0.3014    
subcat_groupedWorkspace Enablement   0.21077    0.32381   0.651   0.5162    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 1.035 on 139 degrees of freedom
Multiple R-squared:  0.03994,   Adjusted R-squared:  -0.001502 
F-statistic: 0.9638 on 6 and 139 DF,  p-value: 0.4522
Code
# ---- Tidy Coefficient Table ----
tidy(model, conf.int = TRUE) %>%
  mutate(across(where(is.numeric), ~round(., 4))) %>%
  kable(caption = "OLS Regression Coefficients (Log-Transformed Outcome)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = FALSE)
OLS Regression Coefficients (Log-Transformed Outcome)
term estimate std.error statistic p.value conf.low conf.high
(Intercept) 4.9976 0.2751 18.1644 0.0000 4.4536 5.5415
urgency_numeric 0.0320 0.2489 0.1286 0.8979 -0.4602 0.5242
priority_numeric -0.0771 0.2305 -0.3343 0.7387 -0.5328 0.3787
is_business_hours -0.3942 0.1964 -2.0075 0.0466 -0.7825 -0.0060
subcat_groupedConnectivity Services 0.0851 0.2212 0.3845 0.7012 -0.3524 0.5225
subcat_groupedOther 0.3428 0.3305 1.0372 0.3014 -0.3107 0.9963
subcat_groupedWorkspace Enablement 0.2108 0.3238 0.6509 0.5162 -0.4295 0.8510
Code
# ---- Model Fit Statistics ----
cat("\n=== Model Fit ===\n")

=== Model Fit ===
Code
cat("R-squared:", round(summary(model)$r.squared, 4), "\n")
R-squared: 0.0399 
Code
cat("Adjusted R-squared:",
    round(summary(model)$adj.r.squared, 4), "\n")
Adjusted R-squared: -0.0015 
Code
f_stat <- summary(model)$fstatistic
f_pval <- pf(f_stat[1], f_stat[2], f_stat[3], lower.tail = FALSE)
cat("F-statistic p-value:", format.pval(f_pval, digits = 4), "\n")
F-statistic p-value: 0.4522 
Code
# ---- Diagnostic Plots ----
par(mfrow = c(2, 2))
plot(model)

Regression Diagnostic Plots
Code
par(mfrow = c(1, 1))
Code
# ---- Variance Inflation Factor (VIF) ----
cat("=== Variance Inflation Factors ===\n")
=== Variance Inflation Factors ===
Code
vif_values <- vif(model)
print(round(vif_values, 4))
                    GVIF Df GVIF^(1/(2*Df))
urgency_numeric   5.2327  1          2.2875
priority_numeric  5.1414  1          2.2675
is_business_hours 1.1470  1          1.0710
subcat_grouped    1.1479  3          1.0233
Code
cat("\nRule of thumb: VIF > 5 indicates problematic multicollinearity.\n")

Rule of thumb: VIF > 5 indicates problematic multicollinearity.
Code
cat("Note: urgency_numeric and priority_numeric may show elevated\n")
Note: urgency_numeric and priority_numeric may show elevated
Code
cat("VIF due to their strong correlation (rho = 0.883)\n")
VIF due to their strong correlation (rho = 0.883)
Code
cat("identified in Section 8.\n")
identified in Section 8.
Code
import statsmodels.formula.api as smf

# ---- Fit OLS Model ----
formula = ('log_resolution_hrs ~ urgency_numeric + priority_numeric + '
           'is_business_hours + C(subcat_grouped)')
model_py = smf.ols(formula, data=df_py).fit()

print("=== OLS Regression Summary ===")
=== OLS Regression Summary ===
Code
print(model_py.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:     log_resolution_hrs   R-squared:                       0.040
Model:                            OLS   Adj. R-squared:                 -0.002
Method:                 Least Squares   F-statistic:                    0.9638
Date:                Tue, 26 May 2026   Prob (F-statistic):              0.452
Time:                        22:30:26   Log-Likelihood:                -208.62
No. Observations:                 146   AIC:                             431.2
Df Residuals:                     139   BIC:                             452.1
Df Model:                           6                                         
Covariance Type:            nonrobust                                         
==============================================================================================================
                                                 coef    std err          t      P>|t|      [0.025      0.975]
--------------------------------------------------------------------------------------------------------------
Intercept                                      4.9976      0.275     18.164      0.000       4.454       5.542
C(subcat_grouped)[T.Connectivity Services]     0.0851      0.221      0.384      0.701      -0.352       0.522
C(subcat_grouped)[T.Other]                     0.3428      0.331      1.037      0.301      -0.311       0.996
C(subcat_grouped)[T.Workspace Enablement]      0.2108      0.324      0.651      0.516      -0.429       0.851
urgency_numeric                                0.0320      0.249      0.129      0.898      -0.460       0.524
priority_numeric                              -0.0771      0.231     -0.334      0.739      -0.533       0.379
is_business_hours                             -0.3942      0.196     -2.008      0.047      -0.782      -0.006
==============================================================================
Omnibus:                        5.595   Durbin-Watson:                   1.147
Prob(Omnibus):                  0.061   Jarque-Bera (JB):                5.748
Skew:                           0.473   Prob(JB):                       0.0565
Kurtosis:                       2.773   Cond. No.                         13.5
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
Code
import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# ---- Residuals vs Fitted ----
axes[0, 0].scatter(model_py.fittedvalues, model_py.resid,
                    alpha=0.6, edgecolors='k', linewidths=0.5)
axes[0, 0].axhline(y=0, color='red', linestyle='--')
axes[0, 0].set_xlabel('Fitted Values')
axes[0, 0].set_ylabel('Residuals')
axes[0, 0].set_title('Residuals vs Fitted')

# ---- Q-Q Plot ----
sp_stats.probplot(model_py.resid, dist='norm', plot=axes[0, 1])
((array([-2.5945072 , -2.27359299, -2.08958675, -1.95723873, -1.85229219,
       -1.76449697, -1.6885129 , -1.6211882 , -1.56050008, -1.50506946,
       -1.45391206, -1.40629967, -1.36167768, -1.31961349, -1.27976281,
       -1.24184696, -1.20563696, -1.1709423 , -1.13760265, -1.10548172,
       -1.07446264, -1.04444436, -1.01533888, -0.98706906, -0.95956686,
       -0.93277196, -0.90663056, -0.88109452, -0.8561205 , -0.83166936,
       -0.80770562, -0.78419698, -0.76111395, -0.73842953, -0.71611888,
       -0.69415914, -0.67252916, -0.65120936, -0.63018153, -0.60942874,
       -0.58893518, -0.56868603, -0.54866744, -0.52886635, -0.5092705 ,
       -0.48986829, -0.47064877, -0.45160157, -0.43271682, -0.41398515,
       -0.39539764, -0.37694576, -0.35862133, -0.34041655, -0.32232391,
       -0.30433617, -0.28644637, -0.26864779, -0.25093392, -0.23329844,
       -0.21573523, -0.19823832, -0.18080189, -0.16342027, -0.14608788,
       -0.12879927, -0.11154908, -0.09433201, -0.07714287, -0.05997649,
       -0.04282777, -0.02569163, -0.00856304,  0.00856304,  0.02569163,
        0.04282777,  0.05997649,  0.07714287,  0.09433201,  0.11154908,
        0.12879927,  0.14608788,  0.16342027,  0.18080189,  0.19823832,
        0.21573523,  0.23329844,  0.25093392,  0.26864779,  0.28644637,
        0.30433617,  0.32232391,  0.34041655,  0.35862133,  0.37694576,
        0.39539764,  0.41398515,  0.43271682,  0.45160157,  0.47064877,
        0.48986829,  0.5092705 ,  0.52886635,  0.54866744,  0.56868603,
        0.58893518,  0.60942874,  0.63018153,  0.65120936,  0.67252916,
        0.69415914,  0.71611888,  0.73842953,  0.76111395,  0.78419698,
        0.80770562,  0.83166936,  0.8561205 ,  0.88109452,  0.90663056,
        0.93277196,  0.95956686,  0.98706906,  1.01533888,  1.04444436,
        1.07446264,  1.10548172,  1.13760265,  1.1709423 ,  1.20563696,
        1.24184696,  1.27976281,  1.31961349,  1.36167768,  1.40629967,
        1.45391206,  1.50506946,  1.56050008,  1.6211882 ,  1.6885129 ,
        1.76449697,  1.85229219,  1.95723873,  2.08958675,  2.27359299,
        2.5945072 ]), array([-2.02761724, -1.99561526, -1.99561526, -1.91809096, -1.87351653,
       -1.4670152 , -1.38332084, -1.26610043, -1.26610043, -1.25126452,
       -1.15755842, -1.1440017 , -1.12555644, -1.12555644, -1.12555644,
       -1.04049581, -1.04049581, -0.98909472, -0.95039906, -0.95039906,
       -0.95039906, -0.86910681, -0.85703839, -0.85703839, -0.85703839,
       -0.85703839, -0.85703839, -0.7633323 , -0.7633323 , -0.73133032,
       -0.73133032, -0.73133032, -0.73133032, -0.73133032, -0.73133032,
       -0.73133032, -0.73133032, -0.73133032, -0.73133032, -0.73133032,
       -0.73133032, -0.73133032, -0.73133032, -0.73133032, -0.73133032,
       -0.72678032, -0.72678032, -0.71543552, -0.66703268, -0.64626968,
       -0.64626968, -0.64626968, -0.64626968, -0.64626968, -0.64626968,
       -0.64626968, -0.64171969, -0.5903186 , -0.5903186 , -0.56104779,
       -0.45826227, -0.45826227, -0.41153166, -0.36455618, -0.36455618,
       -0.36299034, -0.3325542 , -0.3325542 , -0.3325542 , -0.3325542 ,
       -0.3325542 , -0.3325542 , -0.3325542 , -0.32431086, -0.2979703 ,
       -0.24749356, -0.24749356, -0.14380262, -0.10642971,  0.0169472 ,
        0.02856004,  0.0346533 ,  0.07468377,  0.07561602,  0.10991503,
        0.17064841,  0.19678308,  0.20001178,  0.27620092,  0.31523732,
        0.31882788,  0.39227102,  0.43553121,  0.44069557,  0.44358137,
        0.46876577,  0.47154976,  0.51799962,  0.55214397,  0.55249245,
        0.55685846,  0.58367921,  0.5877553 ,  0.5877553 ,  0.63734542,
        0.67281593,  0.80944163,  0.83077904,  0.83283125,  0.83838072,
        0.84144361,  0.84144361,  0.84144361,  0.84144361,  0.84144361,
        0.89736529,  0.89736529,  0.89736529,  0.99107139,  0.99336071,
        1.01398576,  1.02307337,  1.02307337,  1.02307337,  1.02307337,
        1.03168668,  1.06858544,  1.08070332,  1.09444392,  1.108134  ,
        1.2387148 ,  1.27264521,  1.28201594,  1.56611593,  1.58519439,
        1.63699053,  1.6434666 ,  1.76922812,  1.79306411,  1.80006226,
        1.83019376,  1.94286258,  2.46553164,  2.64251871,  2.70156244,
        2.71037492])), (np.float64(1.0052092532162988), np.float64(-1.3135103473602433e-15), np.float64(0.9807541145236024)))
Code
axes[0, 1].set_title('Normal Q-Q Plot of Residuals')

# ---- Scale-Location ----
axes[1, 0].scatter(model_py.fittedvalues,
                    np.sqrt(np.abs(model_py.resid)),
                    alpha=0.6, edgecolors='k', linewidths=0.5)
axes[1, 0].set_xlabel('Fitted Values')
axes[1, 0].set_ylabel('Sqrt(|Residuals|)')
axes[1, 0].set_title('Scale-Location Plot')

# ---- Histogram of Residuals ----
axes[1, 1].hist(model_py.resid, bins=20,
                color='#2C6FAC', edgecolor='white', alpha=0.85)
axes[1, 1].set_xlabel('Residuals')
axes[1, 1].set_ylabel('Frequency')
axes[1, 1].set_title('Distribution of Residuals')

plt.suptitle('Regression Diagnostic Plots',
             fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

Code
from statsmodels.stats.outliers_influence import variance_inflation_factor
from patsy import dmatrices

# ---- VIF Calculation ----
print("=== Variance Inflation Factors ===")
=== Variance Inflation Factors ===
Code
y, X = dmatrices(formula, data=df_py, return_type='dataframe')
for i, col in enumerate(X.columns[1:], 1):  # skip intercept
    vif_val = variance_inflation_factor(X.values, i)
    print(f"  {col}: VIF = {vif_val:.4f}")
  C(subcat_grouped)[T.Connectivity Services]: VIF = 1.6419
  C(subcat_grouped)[T.Other]: VIF = 1.2905
  C(subcat_grouped)[T.Workspace Enablement]: VIF = 1.3942
  urgency_numeric: VIF = 5.2327
  priority_numeric: VIF = 5.1414
  is_business_hours: VIF = 1.1470

9.5 Interpretation

9.5.1 Model Fit

The overall model is not statistically significant (F-test p = 0.452), with an R² of approximately 0.04. This means the model explains only 4% of the variance in log-transformed resolution windows. The adjusted R² is near zero (or slightly negative), confirming that the predictors collectively offer negligible explanatory power beyond chance.

9.5.2 Coefficient Interpretation

While the overall model is weak, the individual coefficients are instructive:

  • Urgency numeric: The coefficient direction may appear counterintuitive given the correlation analysis. However, the high collinearity between urgency and priority means their individual coefficients are unreliable when both are in the model.
  • Priority numeric: Similarly affected by multicollinearity with urgency.
  • Business hours: Tickets created during business hours show a slight negative association with resolution time (shorter windows), though this effect is not statistically significant.
  • Sub-category: Different service categories show varying intercepts, but none are statistically distinguishable from the reference category.

9.5.3 Multicollinearity

The VIF analysis is expected to reveal elevated values for urgency_numeric and priority_numeric, reflecting the \(\rho\) = 0.883 correlation identified in Section 8. This confirms the recommendation from the correlation analysis to either consolidate these variables or remove one from future models.

### Business Implication

The weak regression result is itself one of the **most important findings** of this analysis. It tells us that:

> **The information currently recorded in SysAid at the time of ticket creation — urgency, priority, category, and time of day — does not meaningfully predict how long an incident will take to resolve.**

In the language of analytics maturity models, this places our current ITSM data capability at **Level 1 (Descriptive)** — we can report what happened, but we cannot predict what will happen. To reach **Level 2 (Predictive)**, we need to capture the variables that actually drive resolution timelines. Based on my operational experience and the gaps revealed by this analysis, those variables fall into two categories:

**Factors not currently captured in SysAid:**

| Missing Variable | Expected Impact on R² | Ease of Capture |
|:---|:---|:---|
| `vendor_dependency` (Yes/No) | High — vendor-dependent tickets are 3–5x slower | Easy — checkbox at triage |
| `num_affected_users` (count) | Medium — drives actual business urgency | Easy — numeric field |
| `escalation_count` (count) | High — each handoff adds delay | Auto-tracked by SysAid |
| `root_cause_category` | Medium — enables pattern-based prediction | Moderate — requires training |
| `actual_close_datetime` | Critical — enables true MTTR calculation | Easy — already in SysAid, needs export |

**Factors only known after investigation begins:**

- Root cause complexity (simple misconfiguration vs. design flaw)
- Whether a change request / maintenance window is required
- Third-party coordination requirements (ISPs, equipment vendors, landlords)

**Implementation roadmap:** I recommend a phased approach — (1) immediately add `vendor_dependency` and `num_affected_users` to the SysAid ticket form (one-day configuration change), (2) enable `escalation_count` tracking in SysAid's workflow engine (requires IT Service Management team involvement), and (3) after 6 months of enriched data collection, re-run this regression analysis. Based on similar ITSM enrichment projects documented in the literature, the expected improvement is an R² of 0.25–0.40, which would represent a meaningful predictive capability for SLA management.

10 Integrated Findings

The five analytical techniques applied in this study converge on a coherent and operationally significant set of conclusions about IT incident management at IHS Towers.

10.1 1. The Resolution Window is Highly Variable and Right-Skewed

EDA (Section 5) and Visualisation (Section 6) both reveal that resolution windows range from 12.5 hours to nearly 3,000 hours, with a mean (217 hours) approximately three times the median (74 hours). This right skew — driven by 12 identified outliers — means that average-based SLA targets are misleading. A median-based or percentile-based target (e.g., “80% of incidents resolved within 240 hours”) would better reflect operational reality.

10.2 2. Urgency Classification Does Not Drive Resolution Outcomes

This finding is supported by three independent lines of evidence:

  • Hypothesis testing (Section 7): The Kruskal-Wallis test found no significant difference in resolution windows across urgency levels (p = 0.251, \(\epsilon^2\) ≈ 0).
  • Correlation analysis (Section 8): The Spearman correlation between urgency and resolution window is negligible (\(\rho\) = -0.038, p = 0.650).
  • Regression (Section 9): Urgency does not emerge as a significant predictor of resolution time in the multivariate model.

The convergence of three techniques on this finding strengthens confidence in its validity. The implication is not that urgency should be abandoned, but that the current triage system lacks a mechanism to translate urgency into prioritised action at the resolution stage.

10.3 3. Urgency and Priority are Redundant

Correlation analysis (Section 8) identified a very strong positive monotonic relationship between urgency and priority (\(\rho\) = 0.883, p < 0.001). This near-redundancy inflates the classification burden on technicians without providing incremental information. The recommendation is to consolidate these into a single composite severity field, or clearly differentiate their operational definitions.

10.4 4. Ticket Metadata Has Minimal Predictive Power

The regression model (Section 9) explains only 4% of resolution time variance (R² = 0.04, F-test p = 0.452). This confirms that the current SysAid data-capture scheme is insufficient for predictive analytics. To enable data-driven SLA management, the following additional fields should be captured:

Proposed Field Type Rationale
vendor_dependency Binary Whether resolution requires vendor involvement
num_affected_users Numeric Scale of business impact
root_cause_category Categorical Enables root-cause trending
escalation_count Numeric Number of reassignments before resolution
actual_resolution_date DateTime Actual close time (vs. SLA target date)

10.5 5. Operational Patterns Are Clear Even When Statistical Effects Are Weak

While formal tests do not detect significant differences, the visualisations reveal clear operational patterns: Connectivity Services dominance (56% of tickets), business-hours concentration of incident creation, uneven technician workload distribution, and seasonal fluctuation in weekly volumes. These patterns are actionable even without statistical significance — they inform staffing, automation priorities, and training investments.

10.6 Summary Recommendation

The strongest recommendation emerging from this integrated analysis is a dual-track improvement:

  1. Short-term — SLA policy reform: Replace mean-based SLA targets with percentile-based targets (e.g., P50, P80, P95 resolution times) that account for the skewed distribution. Review the urgency-priority classification scheme to eliminate redundancy and establish clear escalation protocols tied to each level.

  2. Long-term — Data enrichment: Expand the SysAid data-capture template to include the fields proposed above. This investment in data quality will enable future predictive models that can flag at-risk tickets in real time, moving the team from reactive to proactive incident management.

11 Limitations & Further Work

11.1 Limitations

  1. Resolution window vs. actual resolution time: The dataset provides a Due date (SLA target) rather than an actual close date. The resolution_window_hrs variable therefore represents the planned SLA window, not the actual time taken to resolve the incident. This is a meaningful distinction — actual resolution time would be the ideal dependent variable for operational analysis.

  2. Moderate sample size: While 146 observations meet the CS1 minimum requirement (≥100), some sub-categories (e.g., Datacentre & Systems Infrastructure: n = 3; IT Security: n = 5) have very small cell counts. This limited the granularity of sub-category analysis and necessitated grouping in the regression model.

  3. Single-source data: The analysis relies exclusively on SysAid ITSM data. Network performance metrics from PRTG (e.g., bandwidth utilisation, latency, device uptime) were not integrated. Linking ITSM tickets to contemporaneous network telemetry could substantially improve predictive power.

  4. Constant variables: Three columns — Status (all Closed), SR Type (all Incident), and Category (all Information Technology) — showed zero variance and could not contribute to the analysis. This reflects the filtering applied during export.

  5. Temporal scope: The 13-month window (March 2025 – April 2026) may not capture seasonal patterns that span multiple years. A longer time series would enable more robust trend analysis.

  6. Absence of free-text analysis: The Title field contains rich descriptive text (e.g., “MPLS IS DOWN”, “VPN Access Failure”) that was not analysed in this CS1 scope. Text analytics (CS3 Technique 1) could extract additional categorical or sentiment features.

11.2 Further Work

  1. Integrate PRTG network monitoring data to add continuous numeric variables (bandwidth utilisation, packet loss, latency) and enable a richer predictive model.

  2. Extract actual resolution timestamps from SysAid to replace the SLA target date with true resolution time — this is the single most impactful improvement for analytical accuracy.

  3. Expand the dataset to include 2+ years of data to enable time-series decomposition and forecasting of incident volumes.

  4. Conduct text mining on the Title and description fields to automatically categorise incidents by root cause, affected system, and geographic location.

  5. Apply classification models (CS2 Technique 1) to predict SLA breach risk at the time of ticket creation, enabling proactive intervention.

  6. Perform workload optimisation (CS3 Technique 5) to model equitable ticket assignment across technicians using linear programming.

12 References

Adi, B. (2024). Data analytics for business. Lagos Business School.

Allaire, J. J., Teague, C., Scheidegger, C., Xie, Y., & Dervieux, C. (2024). Quarto (Version 1.4). https://quarto.org

Field, A., Miles, J., & Field, Z. (2012). Discovering statistics using R. SAGE Publications.

James, G., Witten, D., Hastie, T., & Tibshirani, R. (2021). An introduction to statistical learning: With applications in R (2nd ed.). Springer.

McKinney, W. (2022). Python for data analysis (3rd ed.). O’Reilly Media.

R Core Team. (2024). R: A language and environment for statistical computing. R Foundation for Statistical Computing. https://www.R-project.org/

Seabold, S., & Perktold, J. (2010). statsmodels: Econometric and statistical modeling with Python. Proceedings of the 9th Python in Science Conference, 57–61.

Tufte, E. R. (2001). The visual display of quantitative information (2nd ed.). Graphics Press.

Tukey, J. W. (1977). Exploratory data analysis. Addison-Wesley.

Virtanen, P., Gommers, R., Oliphant, T. E., Haberland, M., Reddy, T., Cournapeau, D., … & van der Walt, S. J. (2020). SciPy 1.0: Fundamental algorithms for scientific computing in Python. Nature Methods, 17(3), 261–272. https://doi.org/10.1038/s41592-019-0686-2

Waskom, M. (2021). seaborn: Statistical data visualization. Journal of Open Source Software, 6(60), 3021. https://doi.org/10.21105/joss.03021

Wickham, H. (2016). ggplot2: Elegant graphics for data analysis (2nd ed.). Springer. https://ggplot2.tidyverse.org

Wickham, H., Averick, M., Bryan, J., Chang, W., McGowan, L. D., François, R., Grolemund, G., Hayes, A., Henry, L., Hester, J., Kuhn, M., Pedersen, T. L., Miller, E., Bache, S. M., Müller, K., Ooms, J., Robinson, D., Seidel, D. P., Spinu, V., … Yutani, H. (2019). Welcome to the tidyverse. Journal of Open Source Software, 4(43), 1686. https://doi.org/10.21105/joss.01686

Wickham, H., & Grolemund, G. (2017). R for data science: Import, tidy, transform, visualize, and model data. O’Reilly Media.

13 Session Information

The following session information documents the exact software environment used to produce this analysis, ensuring full reproducibility.

Code
# ---- R Session Information for Reproducibility ----
sessionInfo()
R version 4.5.2 (2025-10-31 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26100)

Matrix products: default
  LAPACK version 3.12.1

locale:
[1] LC_COLLATE=English_United States.utf8 
[2] LC_CTYPE=English_United States.utf8   
[3] LC_MONETARY=English_United States.utf8
[4] LC_NUMERIC=C                          
[5] LC_TIME=English_United States.utf8    

time zone: Africa/Lagos
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] broom_1.0.13       car_3.1-5          carData_3.0-6      ggcorrplot_0.1.4.1
 [5] scales_1.4.0       kableExtra_1.4.0   knitr_1.51         lubridate_1.9.5   
 [9] forcats_1.0.1      stringr_1.6.0      dplyr_1.2.1        purrr_1.2.2       
[13] readr_2.2.0        tidyr_1.3.2        tibble_3.3.1       ggplot2_4.0.3     
[17] tidyverse_2.0.0   

loaded via a namespace (and not attached):
 [1] gtable_0.3.6       xfun_0.57          htmlwidgets_1.6.4  lattice_0.22-7    
 [5] tzdb_0.5.0         vctrs_0.7.2        tools_4.5.2        generics_0.1.4    
 [9] parallel_4.5.2     proxy_0.4-29       pkgconfig_2.0.3    Matrix_1.7-4      
[13] RColorBrewer_1.1-3 S7_0.2.1           lifecycle_1.0.5    compiler_4.5.2    
[17] farver_2.1.2       textshaping_1.0.5  htmltools_0.5.9    class_7.3-23      
[21] yaml_2.3.12        Formula_1.2-5      pillar_1.11.1      crayon_1.5.3      
[25] abind_1.4-8        nlme_3.1-168       tidyselect_1.2.1   digest_0.6.39     
[29] stringi_1.8.7      reshape2_1.4.5     labeling_0.4.3     splines_4.5.2     
[33] fastmap_1.2.0      grid_4.5.2         cli_3.6.6          magrittr_2.0.4    
[37] e1071_1.7-17       withr_3.0.2        backports_1.5.0    bit64_4.6.0-1     
[41] timechange_0.4.0   rmarkdown_2.31     bit_4.6.0          otel_0.2.0        
[45] reticulate_1.46.0  png_0.1-9          hms_1.1.4          evaluate_1.0.5    
[49] viridisLite_0.4.3  mgcv_1.9-3         rlang_1.2.0        Rcpp_1.1.1        
[53] glue_1.8.0         xml2_1.5.2         svglite_2.2.2      rstudioapi_0.18.0 
[57] vroom_1.7.0        jsonlite_2.0.0     plyr_1.8.9         R6_2.6.1          
[61] systemfonts_1.3.2 
Code
# ---- Python Session Information for Reproducibility ----
import sys
import pandas as pd
import numpy as np
import scipy
import statsmodels
import matplotlib
import seaborn as sns

print(f"Python version: {sys.version}")
Python version: 3.14.5 (tags/v3.14.5:5607950, May 10 2026, 10:43:50) [MSC v.1944 64 bit (AMD64)]
Code
print(f"pandas: {pd.__version__}")
pandas: 3.0.3
Code
print(f"numpy: {np.__version__}")
numpy: 2.4.6
Code
print(f"scipy: {scipy.__version__}")
scipy: 1.17.1
Code
print(f"statsmodels: {statsmodels.__version__}")
statsmodels: 0.14.6
Code
print(f"matplotlib: {matplotlib.__version__}")
matplotlib: 3.10.9
Code
print(f"seaborn: {sns.__version__}")
seaborn: 0.13.2

14 Appendix: AI Usage Statement

14.1 Declaration of AI-Assisted Tool Usage

In preparing this capstone submission, I used Microsoft Copilot as a coding assistant to accelerate the drafting of R and Python code chunks, generate boilerplate Quarto document structure, and refine data visualisation formatting. All analytical decisions — including the selection of statistical tests, the choice of Spearman over Pearson correlation, the decision to log-transform the dependent variable, the interpretation of non-significant results, and the formulation of business recommendations — were made independently by the author based on the course material (Adi, 2024) and referenced textbooks. The AI tool was used as a productivity aid, not as a substitute for analytical judgement. All code was reviewed, tested, and understood by the author prior to submission.