RTHOR Method Validation: Hubert & Arabie (1987)¶
This example demonstrates that the rthor package correctly implements the
randomization test described in the seminal paper:
Hubert, L. J., & Arabie, P. (1987). Evaluating order hypotheses within proximity matrices. Psychological Bulletin, 102(1), 172-178.
We replicate the paper's example (Table 1, page 174) to verify our implementation produces the expected results.
Background: The RTHOR Method¶
RTHOR (Randomization Test of Hypothesized Order Relations) tests whether correlation matrices conform to a hypothesized ordering of variables using a permutation-based randomization test.
The method (Hubert & Arabie, 1987):
- Count how many order predictions are satisfied by observed data
- Generate all possible permutations of variable labels (or sample when n! > 50,000)
- For each permutation, count how many predictions would be satisfied
- The p-value is the proportion of permutations with equal or better fit
This approach preserves the structural integrity of order predictions while testing against a proper null distribution.
The Example: Holland's Personality Circumplex¶
Holland's Theory¶
Holland (1966, 1973) proposed that six personality types organize in a circular (circumplex) structure:
(1) R
/ \
(6) C (2) I
| |
(5) E (3) A
\ /
(4) S
Where:
- R = Realistic
- I = Investigative
- A = Artistic
- S = Social
- E = Enterprising
- C = Conventional
Circumplex Hypothesis¶
If this circumplex is valid, correlations should follow this pattern:
- Adjacent pairs (RI, IA, AS, SE, EC, CR) should have the highest correlations
- Alternate pairs (RA, AE, ER, IS, SC, CI) should have intermediate correlations
- Opposite pairs (IE, RS, CA) should have the lowest correlations
This generates 72 order predictions using the four-argument function F_H (comparing all pairs of object-pairs).
Note on Dissimilarity Convention¶
The paper displays data as dissimilarities (1 - r) because (page 172):
"Proximity is assumed to be symmetric and nonnegative...and is keyed as a dissimilarity measure so that larger positive values suggest greater degrees of dissimilarity. Thus, if r_ij refers to the product-moment correlation between O_i and O_j, 1 - r_ij would have the form of a symmetric dissimilarity measure; in fact, this measure is the one used in the example given below."
However, both the R implementation (RTHORR) and our Python implementation work with raw correlations where higher values = more similarity. The order predictions are structured accordingly:
- Adjacent pairs should have higher correlations (more similar)
- Opposite pairs should have lower correlations (less similar)
import numpy as np
import pandas as pd
from rich import print # noqa: A004
from rich.console import Console
from rich.table import Table
import rthor
# Set random seed for reproducibility
np.random.seed(42)
Replicating Table 1 from the Paper¶
The paper presents intercorrelations for 2,433 women on the Vocational Preference Inventory (Table 1, page 174).
# Table 1 displays "1 - r" values (dissimilarities)
# We convert back to correlations for our implementation
dissimilarities = np.array(
[
[0.00, 0.46, 0.64, 0.70, 0.57, 0.71], # R
[0.46, 0.00, 0.56, 0.68, 0.74, 0.92], # I
[0.64, 0.56, 0.00, 0.60, 0.59, 0.98], # A
[0.70, 0.68, 0.60, 0.00, 0.54, 0.80], # S
[0.57, 0.74, 0.59, 0.54, 0.00, 0.48], # E
[0.71, 0.92, 0.98, 0.80, 0.48, 0.00], # C
]
)
# Convert dissimilarities to correlations
correlations = 1 - dissimilarities
# Display as DataFrame for readability
types = ["R", "I", "A", "S", "E", "C"]
corr_df = pd.DataFrame(correlations, index=types, columns=types)
print("\nCorrelation Matrix (from Table 1):")
print(corr_df.round(2))
Correlation Matrix (from Table 1):
R I A S E C R 1.00 0.54 0.36 0.30 0.43 0.29 I 0.54 1.00 0.44 0.32 0.26 0.08 A 0.36 0.44 1.00 0.40 0.41 0.02 S 0.30 0.32 0.40 1.00 0.46 0.20 E 0.43 0.26 0.41 0.46 1.00 0.52 C 0.29 0.08 0.02 0.20 0.52 1.00
Visual Verification of Pattern¶
Let's verify the circumplex pattern by examining correlation magnitudes:
# Define pair types according to circumplex model
adjacent_pairs = [
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(5, 0),
] # RI, IA, AS, SE, EC, CR
alternate_pairs = [
(0, 2),
(2, 4),
(4, 0),
(1, 3),
(3, 5),
(5, 1),
] # RA, AE, ER, IS, SC, CI
opposite_pairs = [(0, 3), (1, 4), (2, 5)] # RS, IE, CA
# Extract correlations for each type
adjacent_corrs = [correlations[i, j] for i, j in adjacent_pairs]
alternate_corrs = [correlations[i, j] for i, j in alternate_pairs]
opposite_corrs = [correlations[i, j] for i, j in opposite_pairs]
Pattern Analysis¶
Adjacent pairs (should be HIGHEST correlations)
print(
f" Mean: {np.mean(adjacent_corrs):.3f}, "
f"Range: [{min(adjacent_corrs):.3f}, {max(adjacent_corrs):.3f}]"
)
print(f" Values: {[f'{c:.3f}' for c in adjacent_corrs]}")
Mean: 0.442, Range: [0.290, 0.540]
Values: ['0.540', '0.440', '0.400', '0.460', '0.520', '0.290']
Alternate pairs (should be INTERMEDIATE correlations)
print(
f" Mean: {np.mean(alternate_corrs):.3f}, "
f"Range: [{min(alternate_corrs):.3f}, {max(alternate_corrs):.3f}]"
)
print(f" Values: {[f'{c:.3f}' for c in alternate_corrs]}")
Mean: 0.300, Range: [0.080, 0.430]
Values: ['0.360', '0.410', '0.430', '0.320', '0.200', '0.080']
Opposite pairs (should be LOWEST correlations):
print(
f" Mean: {np.mean(opposite_corrs):.3f}, "
f"Range: [{min(opposite_corrs):.3f}, {max(opposite_corrs):.3f}]"
)
print(f" Values: {[f'{c:.3f}' for c in opposite_corrs]}")
Mean: 0.193, Range: [0.020, 0.300]
Values: ['0.300', '0.260', '0.020']
✓ Pattern consistent with circumplex: Adjacent > Alternate > Opposite
Running the RTHOR Test¶
Now we test this correlation matrix against the circumplex hypothesis:
# Run RTHOR test with circular6 ordering
result = rthor.test(
data=correlations,
order="circular6",
labels=["Rounds et al. 1979"],
print_results=True,
)
RTHOR Test Results 1 matrix • 6 variables • 72 predictions • 720 permutations ╭────────────────────────┬────┬───────┬────────────────┬──────────────┬─────────────┬─────────────╮ │ Matrix │ │ CI │ Interpretation │ Significance │ Satisfied │ Violated │ ├────────────────────────┼────┼───────┼────────────────┼──────────────┼─────────────┼─────────────┤ │ [1] Rounds et al. 1979 │ ↗ │ 0.694 │ Good fit │ p<.05 * │ 61/72 (85%) │ 11/72 (15%) │ ╰────────────────────────┴────┴───────┴────────────────┴──────────────┴─────────────┴─────────────╯ ℹ️ Higher CI values indicate better fit (range: -1 to +1)
Validating Against Paper's Expected Results¶
According to the paper (page 173):
- Total predictions: 72 (from four-argument function F_H)
- Violations observed: 11
- Agreements observed: 61
- p-value: 12/720 = 1/60 ≈ 0.0167
The paper states:
there are 11 violations of the 72 order conjectures characterized by F_H
And (page 175, Table 2):
Using 6 and 11 violations (or 42 and 61 agreements), respectively, both p values are 12/720 = 1/60 = .02
print(f" Total predictions: {result['predictions'].iloc[0]}")
print(f" Agreements: {result['agreements'].iloc[0]}")
violations = (
result["predictions"].iloc[0]
- result["agreements"].iloc[0]
- result["ties"].iloc[0]
)
print(f" Violations: {violations}")
print(f" p-value: {result['p_value'].iloc[0]:.4f}")
Total predictions: 72
Agreements: 61
Violations: 11
p-value: 0.0167
Check if results match¶
expected_agreements = 61
observed_agreements = result["agreements"][0]
expected_pvalue = 12 / 720
observed_pvalue = result["p_value"][0]
print("\n=== Verification Status ===")
if result["predictions"].iloc[0] == 72:
print("✓ Predictions count matches (72)")
else:
print(
f"✗ Predictions count mismatch: expected 72, got {result['predictions'].iloc[0]}"
)
if observed_agreements == expected_agreements:
print("✓ Agreements match (61)")
else:
print(
f"✗ Agreements mismatch: expected {expected_agreements}, got {observed_agreements}"
)
if abs(observed_pvalue - expected_pvalue) < 0.001:
print("✓ p-value matches (~0.0167)")
else:
print(
f"✗ p-value mismatch: expected {expected_pvalue:.4f}, got {observed_pvalue:.4f}"
)
print("\n✓ Implementation validated against Hubert & Arabie (1987) Table 1")
=== Verification Status ===
✓ Predictions count matches (72)
✓ Agreements match (61)
✓ p-value matches (~0.0167)
✓ Implementation validated against Hubert & Arabie (1987) Table 1
Understanding the Violations¶
The paper identifies specific violations (page 173-174):
"For T_H, the following pairs are in violation: {(1, 6), (1, 3)}, {(1, 6), (1, 4)}, {(1, 6), (1, 5)}, {(2, 6), (2, 5)}, {(3, 4), (3, 5)}, and {(4, 6), (4, 1)}."
These violations primarily involve the Conventional (C) type, suggesting some deviation from perfect circumplex structure.
Sample Violations from Paper¶
The paper (p. 173-174) identifies these violations: (These show the Conventional type has unexpected relationships)
- (R,C) vs (R,A): r(R,C)=0.29 should be > r(R,A)=0.36
- (R,C) vs (R,S): r(R,C)=0.29 should be > r(R,S)=0.30
- (R,C) vs (R,E): r(R,C)=0.29 should be > r(R,E)=0.43
- (I,C) vs (I,E): r(I,C)=0.08 should be > r(I,E)=0.26
- (A,S) vs (A,E): r(A,S)=0.40 should be > r(A,E)=0.41
- (S,C) vs (S,R): r(S,C)=0.20 should be > r(S,R)=0.30
Despite these violations, the overall pattern still strongly supports the circumplex hypothesis (p < 0.02).
The Correspondence Index (CI)¶
The paper (page 176, Equation 3) defines the Correspondence Index:
$$ CI = \frac{A - D}{A + D + T} $$
Where:
- A = agreements (predictions satisfied)
- D = disagreements (violations)
- T = ties (equal correlations)
This provides an effect size measure ranging from -1 (perfect disagreement) to +1 (perfect agreement).
Calculate CI components¶
A = result["agreements"].iloc[0]
T = result["ties"].iloc[0]
D = result["predictions"].iloc[0] - A - T
CI = result["ci"].iloc[0]
# Create rich table for CI breakdown
console = Console()
# Create components table
ci_table = Table(
title="Correspondence Index Breakdown",
show_header=True,
header_style="bold magenta",
)
ci_table.add_column("Component", style="cyan")
ci_table.add_column("Value", justify="right", style="green")
ci_table.add_column("Description", style="dim")
ci_table.add_row("A (Agreements)", str(A), "Predictions satisfied by data")
ci_table.add_row("D (Disagreements)", str(D), "Predictions violated by data")
ci_table.add_row("T (Ties)", str(T), "Equal correlations (no prediction)")
ci_table.add_row("Total predictions", str(A + D + T), "A + D + T", style="bold")
ci_table.add_row("CI", f"{CI:.3f}", "(A - D) / (A + D + T)", style="bold yellow")
console.print()
console.print(ci_table)
# Verify calculation
manual_ci = (A - D) / (A + D + T)
print(f"\nVerification: Manual calculation = {manual_ci:.3f}")
assert abs(CI - manual_ci) < 1e-10, "CI calculation mismatch!" # noqa: S101
print("✓ CI formula verified")
# Interpret CI
print("\n=== Interpretation ===")
print(f"CI = {CI:.3f} indicates strong support for the circumplex hypothesis.")
print(
f"Approximately {(A / (A + D)) * 100:.1f}% of non-tied predictions are satisfied."
)
Correspondence Index Breakdown ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Component ┃ Value ┃ Description ┃ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ A (Agreements) │ 61 │ Predictions satisfied by data │ │ D (Disagreements) │ 11 │ Predictions violated by data │ │ T (Ties) │ 0 │ Equal correlations (no prediction) │ │ Total predictions │ 72 │ A + D + T │ │ CI │ 0.694 │ (A - D) / (A + D + T) │ └───────────────────┴───────┴────────────────────────────────────┘
Verification: Manual calculation = 0.694
✓ CI formula verified
=== Interpretation ===
CI = 0.694 indicates strong support for the circumplex hypothesis.
Approximately 84.7% of non-tied predictions are satisfied.
Statistical Significance via Permutation Test¶
The randomization test compares the observed fit to what would be expected by chance. For n=6 variables, there are 6! = 720 possible permutations (page 175, Table 2).
# Calculate permutation test statistics
n_extreme = int(result["p_value"].iloc[0] * result["n_permutations"].iloc[0])
expected_agreements = result["predictions"].iloc[0] / 2 # 50% when no ties
observed_pvalue = result["p_value"].iloc[0]
# Create permutation test table
perm_table = Table(
title="Permutation Test Details",
show_header=True,
header_style="bold magenta",
)
perm_table.add_column("Metric", style="cyan")
perm_table.add_column("Value", justify="right", style="green")
perm_table.add_column("Description", style="dim")
perm_table.add_row(
"Total permutations",
str(result["n_permutations"].iloc[0]),
"6! = 720 for n=6 variables",
)
perm_table.add_row(
"Observed agreements",
str(A),
"Predictions satisfied",
style="bold",
)
perm_table.add_row(
"Expected (null H₀)",
f"{expected_agreements:.1f}",
"50% when random ordering",
)
perm_table.add_row(
"Excess agreements",
f"{A - expected_agreements:.1f}",
"Above chance level",
)
perm_table.add_row(
"Permutations ≥ observed",
str(n_extreme),
f"≥ {A} agreements",
)
perm_table.add_row(
"p-value",
f"{observed_pvalue:.4f}",
f"{n_extreme}/{result['n_permutations'].iloc[0]}",
style="bold yellow",
)
console.print()
console.print(perm_table)
print("\n✓ Result is statistically significant (p < 0.05)")
print("✓ The circumplex hypothesis is supported by the data")
Permutation Test Details ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Metric ┃ Value ┃ Description ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ Total permutations │ 720 │ 6! = 720 for n=6 variables │ │ Observed agreements │ 61 │ Predictions satisfied │ │ Expected (null H₀) │ 36.0 │ 50% when random ordering │ │ Excess agreements │ 25.0 │ Above chance level │ │ Permutations ≥ observed │ 12 │ ≥ 61 agreements │ │ p-value │ 0.0167 │ 12/720 │ └─────────────────────────┴────────┴────────────────────────────┘
✓ Result is statistically significant (p < 0.05)
✓ The circumplex hypothesis is supported by the data
Conclusion¶
This example demonstrates that the rthor package:
- ✓ Correctly implements the Hubert & Arabie (1987) randomization test
- ✓ Replicates the paper's example (Table 1) with exact numerical agreement
- ✓ Calculates the Correspondence Index according to the paper's formula
- ✓ Produces results identical to the R RTHORR package
Citation¶
If you use rthor in your research, please cite the original method:
Hubert, L. J., & Arabie, P. (1987). Evaluating order hypotheses within proximity matrices. Psychological Bulletin, 102(1), 172-178. https://doi.org/10.1037/0033-2909.102.1.172
Further Reading¶
- Core Concepts - Detailed methodology explanation
- API Overview - Complete function documentation
- Result Classes - Working with RTHOR results
- Basic Usage - Getting started with rthor
- Advanced Features - Custom orderings and comparisons