Skip to content

Rate Monitoring Module

The Rate Monitoring module provides tools for tracking and analyzing rate changes and rate adequacy over time. It includes methods for calculating rate change indices and assessing rate adequacy.

Core Components

Rate Change

The rate_change module provides tools for tracking and analyzing rate changes:

  • Functions for calculating rate change indices
  • Methods for analyzing the impact of rate changes on a portfolio
  • Tools for visualizing rate change trends

Rate Adequacy

The rate_adequacy module provides tools for assessing the adequacy of rates:

  • Methods for comparing actual vs. expected loss ratios
  • Tools for analyzing rate adequacy by segment
  • Functions for projecting future rate adequacy

Examples

Tracking Rate Changes

from pyre.rate_monitoring.rate_change import rate_change_simple, rate_change_adjusted
import pandas as pd
import matplotlib.pyplot as plt
from datetime import date

# Create sample data for policies with renewals
data = {
    'policy_id': ['P001', 'P001', 'P002', 'P002', 'P003', 'P003'],
    'year': [2022, 2023, 2022, 2023, 2022, 2023],
    'expiring_premium': [None, 10000, None, 15000, None, 8000],
    'renewed_premium': [10000, 10500, 15000, 15450, 8000, 8560],
    'exposure_change': [None, 0, None, 0.05, None, 0]  # 5% exposure increase for P002
}

df = pd.DataFrame(data)

# Calculate simple rate changes
rate_changes = []
adjusted_rate_changes = []

for policy in df['policy_id'].unique():
    policy_data = df[df['policy_id'] == policy].sort_values('year')
    if len(policy_data) > 1:
        expiring = policy_data.iloc[0]['renewed_premium']
        renewed = policy_data.iloc[1]['renewed_premium']
        exposure_change = policy_data.iloc[1]['exposure_change']

        # Simple rate change (no adjustment for exposure)
        simple_change = rate_change_simple(expiring, renewed)

        # Adjusted rate change (accounting for exposure changes)
        if exposure_change:
            adjusted_expiring = expiring * (1 + exposure_change)
            adjusted_change = rate_change_adjusted(expiring, renewed, adjusted_expiring)
        else:
            adjusted_change = simple_change

        rate_changes.append({
            'policy_id': policy,
            'expiring_premium': expiring,
            'renewed_premium': renewed,
            'exposure_change': exposure_change,
            'simple_rate_change': simple_change,
            'adjusted_rate_change': adjusted_change
        })

# Convert to DataFrame for analysis
rate_change_df = pd.DataFrame(rate_changes)

# Print rate changes
print("Rate Changes by Policy:")
for _, row in rate_change_df.iterrows():
    print(f"Policy {row['policy_id']}:")
    print(f"  Expiring Premium: ${row['expiring_premium']:.2f}")
    print(f"  Renewed Premium: ${row['renewed_premium']:.2f}")
    print(f"  Exposure Change: {row['exposure_change'] or 0:.1%}")
    print(f"  Simple Rate Change: {row['simple_rate_change']:.2%}")
    print(f"  Adjusted Rate Change: {row['adjusted_rate_change']:.2%}")

# Calculate portfolio averages
avg_simple_change = rate_change_df['simple_rate_change'].mean()
avg_adjusted_change = rate_change_df['adjusted_rate_change'].mean()
weighted_avg_change = (rate_change_df['adjusted_rate_change'] * rate_change_df['expiring_premium']).sum() / rate_change_df['expiring_premium'].sum()

print("\nPortfolio Rate Change Summary:")
print(f"  Simple Average Rate Change: {avg_simple_change:.2%}")
print(f"  Adjusted Average Rate Change: {avg_adjusted_change:.2%}")
print(f"  Premium-Weighted Average Rate Change: {weighted_avg_change:.2%}")

# Visualize rate changes
plt.figure(figsize=(10, 6))
plt.bar(rate_change_df['policy_id'], rate_change_df['simple_rate_change'], alpha=0.7, label='Simple Rate Change')
plt.bar(rate_change_df['policy_id'], rate_change_df['adjusted_rate_change'], alpha=0.7, label='Adjusted Rate Change')
plt.axhline(y=0, color='r', linestyle='-')
plt.axhline(y=weighted_avg_change, color='g', linestyle='--', label='Weighted Average')
plt.title('Rate Changes by Policy')
plt.xlabel('Policy ID')
plt.ylabel('Rate Change')
plt.legend()
plt.grid(True, axis='y')
plt.tight_layout()
plt.show()

Analyzing Rate Adequacy

from pyre.rate_monitoring.rate_adequacy import rate_adequacy, rate_adequacy_change
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

# Create sample data
np.random.seed(42)
n_policies = 100

# Current period data
current_data = {
    'policy_id': [f'P{i:03d}' for i in range(1, n_policies + 1)],
    'line_of_business': np.random.choice(['Property', 'Casualty', 'Marine'], n_policies),
    'premium': np.random.uniform(5000, 50000, n_policies),
    'incurred_losses': np.random.uniform(2000, 40000, n_policies),
    'indicated_premium': np.random.uniform(4000, 55000, n_policies)
}

# Prior period data (with some rate changes)
prior_data = {
    'policy_id': [f'P{i:03d}' for i in range(1, n_policies + 1)],
    'premium': [p * np.random.uniform(0.9, 1.0) for p in current_data['premium']],
    'incurred_losses': [l * np.random.uniform(0.85, 1.05) for l in current_data['incurred_losses']],
    'indicated_premium': [p * np.random.uniform(0.9, 1.0) for p in current_data['indicated_premium']]
}

# Create DataFrames
current_df = pd.DataFrame(current_data)
prior_df = pd.DataFrame(prior_data)

# Calculate rate adequacy for current period
current_df['adequacy'] = current_df.apply(
    lambda row: rate_adequacy(row['premium'], row['indicated_premium']), 
    axis=1
)

# Calculate rate adequacy for prior period
prior_df['adequacy'] = prior_df.apply(
    lambda row: rate_adequacy(row['premium'], row['indicated_premium']), 
    axis=1
)

# Merge the dataframes to calculate change in adequacy
df = current_df.merge(prior_df, on='policy_id', suffixes=('', '_prior'))
df['adequacy_change'] = df.apply(
    lambda row: rate_adequacy_change(
        row['premium_prior'], row['indicated_premium_prior'],
        row['premium'], row['indicated_premium']
    ),
    axis=1
)

# Calculate actual loss ratios
df['actual_lr'] = df['incurred_losses'] / df['premium']
df['actual_lr_prior'] = df['incurred_losses_prior'] / df['premium_prior']
df['lr_change'] = df['actual_lr'] - df['actual_lr_prior']

# Print summary statistics
print("Rate Adequacy Summary:")
print(f"Average Rate Adequacy (Current): {df['adequacy'].mean():.2%}")
print(f"Average Rate Adequacy (Prior): {df['adequacy_prior'].mean():.2%}")
print(f"Average Change in Adequacy: {df['adequacy_change'].mean():.2%}")
print(f"Median Rate Adequacy (Current): {df['adequacy'].median():.2%}")
print(f"Min Rate Adequacy (Current): {df['adequacy'].min():.2%}")
print(f"Max Rate Adequacy (Current): {df['adequacy'].max():.2%}")

# Analyze rate adequacy by line of business
lob_summary = df.groupby('line_of_business').agg({
    'premium': 'sum',
    'premium_prior': 'sum',
    'incurred_losses': 'sum',
    'incurred_losses_prior': 'sum',
    'adequacy': 'mean',
    'adequacy_prior': 'mean',
    'adequacy_change': 'mean'
}).reset_index()

lob_summary['actual_lr'] = lob_summary['incurred_losses'] / lob_summary['premium']
lob_summary['actual_lr_prior'] = lob_summary['incurred_losses_prior'] / lob_summary['premium_prior']
lob_summary['lr_change'] = lob_summary['actual_lr'] - lob_summary['actual_lr_prior']

print("\nRate Adequacy by Line of Business:")
for _, row in lob_summary.iterrows():
    print(f"{row['line_of_business']}:")
    print(f"  Current Premium: ${row['premium']:.2f}")
    print(f"  Current Loss Ratio: {row['actual_lr']:.2%}")
    print(f"  Current Adequacy: {row['adequacy']:.2%}")
    print(f"  Prior Adequacy: {row['adequacy_prior']:.2%}")
    print(f"  Change in Adequacy: {row['adequacy_change']:.2%}")

# Visualize rate adequacy
plt.figure(figsize=(12, 8))

# Histogram of rate adequacy
plt.subplot(2, 2, 1)
plt.hist(df['adequacy'], bins=20, alpha=0.7, label='Current')
plt.hist(df['adequacy_prior'], bins=20, alpha=0.5, label='Prior')
plt.axvline(1.0, color='r', linestyle='--')
plt.title('Distribution of Rate Adequacy')
plt.xlabel('Rate Adequacy (1.0 = Adequate)')
plt.ylabel('Frequency')
plt.legend()

# Rate adequacy by line of business
plt.subplot(2, 2, 2)
x = np.arange(len(lob_summary))
width = 0.35
plt.bar(x - width/2, lob_summary['adequacy_prior'], width, label='Prior', alpha=0.7)
plt.bar(x + width/2, lob_summary['adequacy'], width, label='Current', alpha=0.7)
plt.axhline(1.0, color='black', linestyle='-')
plt.xticks(x, lob_summary['line_of_business'])
plt.title('Rate Adequacy by Line of Business')
plt.ylabel('Rate Adequacy')
plt.legend()

# Scatter plot of premium vs. rate adequacy
plt.subplot(2, 2, 3)
for lob in df['line_of_business'].unique():
    subset = df[df['line_of_business'] == lob]
    plt.scatter(subset['premium'], subset['adequacy'], alpha=0.7, label=lob)
plt.axhline(1.0, color='r', linestyle='--')
plt.title('Premium vs. Rate Adequacy')
plt.xlabel('Premium')
plt.ylabel('Rate Adequacy')
plt.legend()

# Change in adequacy vs. change in loss ratio
plt.subplot(2, 2, 4)
plt.scatter(df['lr_change'], df['adequacy_change'], alpha=0.5)
plt.axhline(0, color='r', linestyle='--')
plt.axvline(0, color='r', linestyle='--')
plt.title('Change in Loss Ratio vs. Change in Adequacy')
plt.xlabel('Change in Loss Ratio')
plt.ylabel('Change in Rate Adequacy')

plt.tight_layout()
plt.show()

API Reference

Rate Change Metrics Calculation Module

Bodoff, N. (2009). "Measuring Rate Change: Methods and Implications." https://www.casact.org/sites/default/files/database/forum_09wforum_bodoff.pdf

Llloyd's PMDR requirements and calculations

https://assets.lloyds.com/media/04e08389-5b68-42a7-aa66-167df72c0721/PMDR-Instructions-2024-V1.0.pdf https://assets.lloyds.com/assets/pdf-performance-management-pmdr-renewal-scenario-examples/1/pdf-performance-management-PMDR-Renewal-Scenario-Examples.pdf

rate_change_adjusted(expiring_premium, renewed_premium, adjusted_expiring_premium=None)

Calculate rate change, optionally adjusting expiring premium for exposure/terms.

Parameters:

Name Type Description Default
expiring_premium float

Premium charged last year.

required
renewed_premium float

Premium charged this year.

required
adjusted_expiring_premium Optional[float]

Expiring premium adjusted for exposure/terms.

None

Returns:

Name Type Description
float float

Rate change as a decimal.

Source code in src\pyre\rate_monitoring\rate_change.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def rate_change_adjusted(
    expiring_premium: float,
    renewed_premium: float,
    adjusted_expiring_premium: Optional[float] = None
) -> float:
    """
    Calculate rate change, optionally adjusting expiring premium for exposure/terms.

    Args:
        expiring_premium (float): Premium charged last year.
        renewed_premium (float): Premium charged this year.
        adjusted_expiring_premium (Optional[float]): Expiring premium adjusted for exposure/terms.

    Returns:
        float: Rate change as a decimal.
    """
    base = adjusted_expiring_premium if adjusted_expiring_premium is not None else expiring_premium
    if base == 0:
        raise ValueError("Expiring or adjusted expiring premium cannot be zero.")
    return (renewed_premium / base) - 1

rate_change_simple(expiring_premium, renewed_premium)

Calculate simple rate change (no adjustment for exposure or terms).

Parameters:

Name Type Description Default
expiring_premium float

Premium charged last year.

required
renewed_premium float

Premium charged this year.

required

Returns:

Name Type Description
float float

Rate change as a decimal (e.g., 0.10 for 10%).

Source code in src\pyre\rate_monitoring\rate_change.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def rate_change_simple(expiring_premium: float, renewed_premium: float) -> float:
    """
    Calculate simple rate change (no adjustment for exposure or terms).

    Args:
        expiring_premium (float): Premium charged last year.
        renewed_premium (float): Premium charged this year.

    Returns:
        float: Rate change as a decimal (e.g., 0.10 for 10%).
    """
    if expiring_premium == 0:
        raise ValueError("Expiring premium cannot be zero.")
    return (renewed_premium / expiring_premium) - 1

Rate Adequacy Calculation Functions

Implements rate adequacy calculations as described in: Bodoff, N. (2009). "Measuring Rate Change: Methods and Implications." https://www.casact.org/sites/default/files/database/forum_09wforum_bodoff.pdf

rate_adequacy(premium, indicated_premium)

Calculates rate adequacy as the ratio of actual premium to indicated premium.

Parameters:

Name Type Description Default
premium float

Actual charged premium.

required
indicated_premium float

Indicated (actuarially adequate) premium.

required

Returns:

Name Type Description
float float

Rate adequacy (e.g., 1.05 means 5% adequate, 0.95 means 5% inadequate).

Source code in src\pyre\rate_monitoring\rate_adequacy.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def rate_adequacy(premium: float, indicated_premium: float) -> float:
    """
    Calculates rate adequacy as the ratio of actual premium to indicated premium.

    Args:
        premium (float): Actual charged premium.
        indicated_premium (float): Indicated (actuarially adequate) premium.

    Returns:
        float: Rate adequacy (e.g., 1.05 means 5% adequate, 0.95 means 5% inadequate).
    """
    if indicated_premium == 0:
        raise ValueError("Indicated premium cannot be zero.")
    return premium / indicated_premium

rate_adequacy_change(prior_premium, prior_indicated, current_premium, current_indicated)

Calculates the change in rate adequacy between two periods.

Parameters:

Name Type Description Default
prior_premium float

Actual premium in prior period.

required
prior_indicated float

Indicated premium in prior period.

required
current_premium float

Actual premium in current period.

required
current_indicated float

Indicated premium in current period.

required

Returns:

Name Type Description
float float

Change in rate adequacy (e.g., 0.05 means 5% improvement).

Source code in src\pyre\rate_monitoring\rate_adequacy.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def rate_adequacy_change(
    prior_premium: float,
    prior_indicated: float,
    current_premium: float,
    current_indicated: float
) -> float:
    """
    Calculates the change in rate adequacy between two periods.

    Args:
        prior_premium (float): Actual premium in prior period.
        prior_indicated (float): Indicated premium in prior period.
        current_premium (float): Actual premium in current period.
        current_indicated (float): Indicated premium in current period.

    Returns:
        float: Change in rate adequacy (e.g., 0.05 means 5% improvement).
    """
    prior_adequacy = rate_adequacy(prior_premium, prior_indicated)
    current_adequacy = rate_adequacy(current_premium, current_indicated)
    return current_adequacy - prior_adequacy