Swedish and Finnish League Analytics

Beginner 10 min read 1 views Nov 27, 2025

SHL and Liiga: Premier Development Leagues

The Swedish Hockey League (SHL) and Finnish Liiga are consistently among the best development systems for NHL prospects. These leagues emphasize tactical play, two-way responsibility, and skill development, producing a steady stream of NHL talent.

League Characteristics

  • SHL (Sweden): 14 teams, structured defensive systems, high skill level
  • Liiga (Finland): 15 teams, tactical emphasis, physical two-way play
  • Allsvenskan (Sweden): Second-tier development league
  • Mestis (Finland): Finnish second division

SHL/Liiga Translation to NHL

Python: Nordic League Analysis

import pandas as pd
import numpy as np
from sklearn.ensemble import GradientBoostingRegressor
import matplotlib.pyplot as plt

# Load Nordic league data
shl_to_nhl = pd.read_csv('shl_to_nhl_transitions.csv')
liiga_to_nhl = pd.read_csv('liiga_to_nhl_transitions.csv')

# Combine datasets
nordic_transitions = pd.concat([
    shl_to_nhl.assign(league='SHL'),
    liiga_to_nhl.assign(league='Liiga')
])

# Calculate translation factors by league
shl_translation = np.corrcoef(
    shl_to_nhl['league_ppg'],
    shl_to_nhl['nhl_ppg']
)[0, 1]

liiga_translation = np.corrcoef(
    liiga_to_nhl['league_ppg'],
    liiga_to_nhl['nhl_ppg']
)[0, 1]

print("=== Nordic League Translation Factors ===")
print(f"SHL correlation: {shl_translation:.3f}")
print(f"Liiga correlation: {liiga_translation:.3f}")

# Age-specific analysis
def calculate_u23_performance(df):
    """Analyze U23 player performance"""
    u23_players = df[df['age'] < 23]

    if len(u23_players) == 0:
        return None

    return {
        'count': len(u23_players),
        'avg_league_ppg': u23_players['league_ppg'].mean(),
        'avg_nhl_ppg': u23_players['nhl_ppg'].mean(),
        'translation_rate': (u23_players['nhl_ppg'].mean() /
                           u23_players['league_ppg'].mean())
    }

shl_u23 = calculate_u23_performance(shl_to_nhl)
liiga_u23 = calculate_u23_performance(liiga_to_nhl)

print("\n=== U23 Player Analysis ===")
print(f"SHL U23: {shl_u23}")
print(f"Liiga U23: {liiga_u23}")

# Draft position impact
def analyze_by_draft_position(df):
    """Analyze success by draft position"""
    df['draft_category'] = pd.cut(
        df['draft_position'],
        bins=[0, 10, 30, 60, 120, 300],
        labels=['Top 10', 'First Round', 'Top 2 Rounds',
                'Mid Rounds', 'Late/Undrafted']
    )

    return df.groupby('draft_category').agg({
        'nhl_ppg': ['mean', 'count'],
        'league_ppg': 'mean'
    })

shl_draft_analysis = analyze_by_draft_position(shl_to_nhl)
liiga_draft_analysis = analyze_by_draft_position(liiga_to_nhl)

print("\n=== SHL Success by Draft Position ===")
print(shl_draft_analysis)

print("\n=== Liiga Success by Draft Position ===")
print(liiga_draft_analysis)

# Playing style translation
def classify_nordic_style(row):
    """Classify playing style from Nordic leagues"""
    g_ratio = row['goals'] / row['points'] if row['points'] > 0 else 0

    # Nordic leagues reward complete players
    if row['es_points_pct'] >= 0.70:
        base_style = 'Two-Way Forward'
    elif g_ratio >= 0.55:
        base_style = 'Goal Scorer'
    elif g_ratio <= 0.35:
        base_style = 'Playmaker'
    else:
        base_style = 'Balanced Producer'

    return base_style

nordic_transitions['style'] = nordic_transitions.apply(
    classify_nordic_style, axis=1
)

# Style success rates
style_success = nordic_transitions.groupby(['league', 'style']).agg({
    'nhl_ppg': ['mean', 'count']
}).round(3)

print("\n=== NHL Success by Playing Style ===")
print(style_success)

# Current prospects analysis
current_shl = pd.read_csv('current_shl_players.csv')
current_liiga = pd.read_csv('current_liiga_players.csv')

def score_nordic_prospect(row):
    """Comprehensive prospect scoring"""
    score = 0

    # Production (0-40 points)
    ppg = row['points'] / row['games_played']
    if ppg >= 0.70:
        score += 40
    elif ppg >= 0.55:
        score += 32
    elif ppg >= 0.40:
        score += 24
    else:
        score += 16

    # Age factor (0-25 points)
    if row['age'] <= 20:
        score += 25
    elif row['age'] <= 22:
        score += 20
    elif row['age'] <= 24:
        score += 15
    else:
        score += 10

    # Even-strength production (0-20 points)
    es_pct = row['es_points'] / row['points'] if row['points'] > 0 else 0
    if es_pct >= 0.70:
        score += 20
    elif es_pct >= 0.60:
        score += 15
    else:
        score += 10

    # Ice time (indicates trust) (0-10 points)
    if row['toi_per_game'] >= 18:
        score += 10
    elif row['toi_per_game'] >= 15:
        score += 7
    else:
        score += 4

    # Physical maturity (0-5 points)
    if row['age'] >= 20 and row['weight_kg'] >= 85:
        score += 5
    elif row['weight_kg'] >= 80:
        score += 3
    else:
        score += 1

    return score

# Score all current prospects
current_shl['prospect_score'] = current_shl.apply(score_nordic_prospect, axis=1)
current_liiga['prospect_score'] = current_liiga.apply(score_nordic_prospect, axis=1)

# Combine and rank
all_nordic_prospects = pd.concat([
    current_shl.assign(league='SHL'),
    current_liiga.assign(league='Liiga')
])

top_nordic = all_nordic_prospects.sort_values(
    'prospect_score', ascending=False
).head(20)

print("\n=== Top Nordic Prospects ===")
print(top_nordic[[
    'name', 'age', 'league', 'points', 'games_played',
    'toi_per_game', 'prospect_score'
]])

# Project NHL performance
base_translation_factors = {'SHL': 0.36, 'Liiga': 0.34}

def project_nhl_production(row):
    """Project NHL points based on Nordic performance"""
    ppg = row['points'] / row['games_played']
    base_factor = base_translation_factors.get(row['league'], 0.35)

    # Age adjustment
    if row['age'] <= 21:
        age_adj = 1.15
    elif row['age'] <= 23:
        age_adj = 1.10
    elif row['age'] <= 25:
        age_adj = 1.05
    else:
        age_adj = 1.00

    projected_nhl_ppg = ppg * base_factor * age_adj
    projected_points = projected_nhl_ppg * 82

    return projected_points

top_nordic['projected_nhl_points'] = top_nordic.apply(
    project_nhl_production, axis=1
)

print("\n=== NHL Production Projections ===")
print(top_nordic[[
    'name', 'age', 'league', 'points', 'games_played',
    'projected_nhl_points'
]].head(15))

R: Nordic League Visualization

library(tidyverse)
library(patchwork)
library(scales)

# Load Nordic transition data
shl_to_nhl <- read_csv("shl_to_nhl_transitions.csv") %>% mutate(league = "SHL")
liiga_to_nhl <- read_csv("liiga_to_nhl_transitions.csv") %>% mutate(league = "Liiga")

nordic_transitions <- bind_rows(shl_to_nhl, liiga_to_nhl)

# Translation factor analysis
translation_factors <- nordic_transitions %>%
  group_by(league) %>%
  summarise(
    correlation = cor(league_ppg, nhl_ppg),
    n = n(),
    avg_league_ppg = mean(league_ppg),
    avg_nhl_ppg = mean(nhl_ppg)
  )

cat("=== Nordic League Translation Factors ===\n")
print(translation_factors)

# U23 player analysis
u23_analysis <- nordic_transitions %>%
  filter(age < 23) %>%
  group_by(league) %>%
  summarise(
    count = n(),
    avg_league_ppg = mean(league_ppg),
    avg_nhl_ppg = mean(nhl_ppg),
    translation_rate = avg_nhl_ppg / avg_league_ppg
  )

cat("\n=== U23 Player Analysis ===\n")
print(u23_analysis)

# Draft position impact
draft_analysis <- nordic_transitions %>%
  mutate(
    draft_category = cut(
      draft_position,
      breaks = c(0, 10, 30, 60, 120, 300),
      labels = c("Top 10", "First Round", "Top 2 Rounds",
                "Mid Rounds", "Late/Undrafted")
    )
  ) %>%
  group_by(league, draft_category) %>%
  summarise(
    count = n(),
    avg_nhl_ppg = mean(nhl_ppg),
    avg_league_ppg = mean(league_ppg),
    .groups = "drop"
  )

cat("\n=== Success by Draft Position ===\n")
print(draft_analysis)

# Playing style classification
nordic_transitions <- nordic_transitions %>%
  mutate(
    goals_ratio = goals / points,
    style = case_when(
      es_points_pct >= 0.70 ~ "Two-Way Forward",
      goals_ratio >= 0.55 ~ "Goal Scorer",
      goals_ratio <= 0.35 ~ "Playmaker",
      TRUE ~ "Balanced Producer"
    )
  )

# Style success rates
style_success <- nordic_transitions %>%
  group_by(league, style) %>%
  summarise(
    count = n(),
    avg_nhl_ppg = mean(nhl_ppg),
    .groups = "drop"
  )

cat("\n=== NHL Success by Playing Style ===\n")
print(style_success)

# Current prospects
current_shl <- read_csv("current_shl_players.csv") %>% mutate(league = "SHL")
current_liiga <- read_csv("current_liiga_players.csv") %>% mutate(league = "Liiga")

# Prospect scoring function
score_nordic_prospect <- function(points, games_played, age, es_points,
                                 toi_per_game, weight_kg) {
  score <- 0
  ppg <- points / games_played

  # Production (0-40)
  score <- score + case_when(
    ppg >= 0.70 ~ 40,
    ppg >= 0.55 ~ 32,
    ppg >= 0.40 ~ 24,
    TRUE ~ 16
  )

  # Age (0-25)
  score <- score + case_when(
    age <= 20 ~ 25,
    age <= 22 ~ 20,
    age <= 24 ~ 15,
    TRUE ~ 10
  )

  # ES production (0-20)
  es_pct <- ifelse(points > 0, es_points / points, 0)
  score <- score + case_when(
    es_pct >= 0.70 ~ 20,
    es_pct >= 0.60 ~ 15,
    TRUE ~ 10
  )

  # Ice time (0-10)
  score <- score + case_when(
    toi_per_game >= 18 ~ 10,
    toi_per_game >= 15 ~ 7,
    TRUE ~ 4
  )

  # Physical (0-5)
  score <- score + case_when(
    age >= 20 & weight_kg >= 85 ~ 5,
    weight_kg >= 80 ~ 3,
    TRUE ~ 1
  )

  return(score)
}

# Score prospects
all_nordic_prospects <- bind_rows(current_shl, current_liiga) %>%
  rowwise() %>%
  mutate(
    prospect_score = score_nordic_prospect(
      points, games_played, age, es_points, toi_per_game, weight_kg
    )
  ) %>%
  ungroup()

top_nordic <- all_nordic_prospects %>%
  arrange(desc(prospect_score)) %>%
  head(20)

cat("\n=== Top Nordic Prospects ===\n")
print(top_nordic %>%
  select(name, age, league, points, games_played, toi_per_game, prospect_score))

# NHL projection
base_translation <- c("SHL" = 0.36, "Liiga" = 0.34)

all_nordic_prospects <- all_nordic_prospects %>%
  mutate(
    ppg = points / games_played,
    base_factor = base_translation[league],
    age_adj = case_when(
      age <= 21 ~ 1.15,
      age <= 23 ~ 1.10,
      age <= 25 ~ 1.05,
      TRUE ~ 1.00
    ),
    projected_nhl_points = ppg * base_factor * age_adj * 82
  )

cat("\n=== NHL Production Projections ===\n")
print(all_nordic_prospects %>%
  arrange(desc(projected_nhl_points)) %>%
  head(15) %>%
  select(name, age, league, points, games_played, projected_nhl_points))

# Visualization: Translation comparison
p1 <- ggplot(nordic_transitions, aes(x = league_ppg, y = nhl_ppg, color = league)) +
  geom_point(alpha = 0.6, size = 3) +
  geom_smooth(method = "lm", se = TRUE) +
  labs(title = "Nordic League to NHL Translation",
       x = "League PPG", y = "NHL PPG") +
  theme_minimal()

# Visualization: Age distribution of success
p2 <- ggplot(nordic_transitions %>% filter(nhl_ppg >= 0.50),
       aes(x = age, fill = league)) +
  geom_histogram(binwidth = 1, alpha = 0.7, position = "dodge") +
  labs(title = "Age Distribution of Successful Transitions",
       subtitle = "Players with NHL PPG >= 0.50",
       x = "Age at Transition", y = "Count") +
  theme_minimal()

# Combine plots
print(p1 / p2)

Why Nordic Leagues Produce NHL Talent

SHL and Liiga consistently develop NHL-ready players due to their emphasis on complete skill development. Young players must earn ice time against professional competition, learn defensive responsibility, and develop hockey IQ in structured systems. The leagues' translation success is among the highest outside North America.

Nordic League Prospect Indicators

  • Strong even-strength production (70%+ of points)
  • Consistent ice time in top-6 or top-4 role
  • Production maintained across multiple seasons
  • Success in international junior tournaments
  • Two-way responsibility demonstrated

Discussion

Have questions or feedback? Join our community discussion on Discord or GitHub Discussions.