> ## Documentation Index
> Fetch the complete documentation index at: https://docs.effectiveai.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Compare carriers

> Rate one book with two carriers, check what each carrier receives and prices, and compare the premiums on one basis.

Compare carrier premiums on the same market basket or book, then summarize the gaps by segment.
This example aligns coverage and term before comparing, and retains every rating exception alongside the matched population.

[Install Relativities and download the example data](/relativities/setup) before running this page.

<Accordion title="Set up the book and the two carriers">
  The book is the synthetic six-household book in `relativities-examples/ppa/book`. Carrier A
  rates liability and collision per vehicle for a six-month term. Carrier B rates an account for a
  twelve-month term and returns one premium. The sections below inspect what each carrier's `prepare` sends and align the coverage basis.

  ```python theme={null}
  from pathlib import Path

  import pandas as pd

  from relativities import Book, Market, Program, Rater, Study, ppa

  book = Book.read_csv("relativities-examples/ppa/book", schema=ppa.SCHEMA)


  VEHICLE = ["submission_id", "vehicle_id"]


  def years(start, on):
      """Whole years from each start date to each on date."""
      before = on.dt.strftime("%m%d") < start.dt.strftime("%m%d")
      return on.dt.year - start.dt.year - before.astype(int)


  def on_effective_date(book):
      """Each driver with their submission's effective date."""
      return book.drivers.merge(book.submissions[["submission_id", "effective_date"]])


  def nest(book, **lists):
      """Each submission's input: one list of records per keyword, in book order."""
      grouped = {
          name: {sid: rows.drop(columns="submission_id").to_dict("records") for sid, rows in frame.groupby("submission_id", sort=False)}
          for name, frame in lists.items()
      }
      return {sid: {name: grouped[name].get(sid, []) for name in lists} for sid in book.submissions["submission_id"]}


  def carrier_a(book):
      drivers = on_effective_date(book)
      drivers["age"] = years(drivers["birth_date"], drivers["effective_date"])
      coverages = book.vehicle_coverages
      collision = coverages[coverages["coverage_id"] == "collision"][[*VEHICLE, "deductible"]]
      vehicles = book.vehicles.merge(collision, on=VEHICLE, how="left")
      vehicles["collision_deductible"] = vehicles["deductible"].fillna(0).astype(int)
      return nest(
          book,
          drivers=drivers[["submission_id", "age"]],
          vehicles=vehicles[["submission_id", "annual_mileage", "collision_deductible"]],
      )


  def carrier_b(book):
      drivers = on_effective_date(book)
      blank = drivers.loc[drivers["first_licensed_date"].isna(), "driver_id"]
      if len(blank):
          raise ValueError(f"driver {blank.iloc[0]} has no first_licensed_date")
      drivers["years_licensed"] = years(drivers["first_licensed_date"], drivers["effective_date"])
      coverages = book.vehicle_coverages
      comprehensive = coverages[coverages["coverage_id"] == "comprehensive"][VEHICLE]
      autos = book.vehicles.merge(comprehensive.assign(comprehensive=True), on=VEHICLE, how="left")
      autos["comprehensive"] = autos["comprehensive"].eq(True)
      autos = autos.rename(columns={"annual_mileage": "miles"})
      return nest(
          book,
          operators=drivers[["submission_id", "years_licensed"]],
          autos=autos[["submission_id", "miles", "comprehensive"]],
      )


  market = Market(
      {
          "a": Program(Rater.load(Path("relativities-examples/ppa/carrier-a.zip")), carrier_a, schema=ppa.SCHEMA),
          "b": Program(Rater.load(Path("relativities-examples/ppa/carrier-b.zip")), carrier_b, schema=ppa.SCHEMA),
      }
  )
  ```
</Accordion>

```python theme={null}
coverages = book.vehicle_coverages
liability = book.with_tables(vehicle_coverages=coverages[coverages["coverage_id"] == "liability"])
compared = market.rate(liability)
compared["annual"] = compared["premium"].mul(2).fillna(compared["annual_premium"])
annual = compared.pivot(index="submission_id", columns="program", values="annual")
annual
# program             a       b
# submission_id
# S1              600.0   480.0
# S2             1350.0  1176.0
# S3              600.0     NaN
# S4                NaN     NaN
# S5              480.0     NaN
# S6             1680.0  1380.0
```

The basis is liability only, expressed as annualized premium. Carrier B has the lower premium for each
submission that both carriers rate. `NaN` marks a submission that a carrier leaves unrated.

```mermaid theme={null}
flowchart LR
  book["Book: facts as reported"] --> prepA["Carrier A prepare"]
  book --> prepB["Carrier B prepare"]
  prepA --> raterA["Carrier A: age, mileage, collision deductible"]
  prepB --> raterB["Carrier B: years licensed, miles, comprehensive flag"]
  raterA --> outA["premium for a six-month term"]
  raterB --> outB["annual_premium for a twelve-month term"]
```

[Programs and schemas](/relativities/explanation/programs-and-schemas) describes how a market sends one book
to each carrier.

## List the fields each carrier accepts

List each rater's `inputs`:

```python theme={null}
market.programs["a"].rater.inputs["path"].tolist()
# ['drivers', 'drivers[].age', 'vehicles', 'vehicles[].annual_mileage', 'vehicles[].collision_deductible']
market.programs["b"].rater.inputs["path"].tolist()
# ['autos', 'autos[].comprehensive', 'autos[].miles', 'operators', 'operators[].years_licensed']
```

Carrier A accepts a collision deductible. Carrier B accepts a comprehensive flag. The two
premiums cover different coverages before any adjustment.

## Check what each carrier receives

Explain one submission with the market, and read the input each rater received:

```python theme={null}
explained = market.explain(book, "S1")
explained["a"].json_in
# {'drivers': [{'age': 40}], 'vehicles': [{'annual_mileage': 12000, 'collision_deductible': 500}]}
explained["b"].json_in
# {'operators': [{'years_licensed': 20}], 'autos': [{'miles': 12000, 'comprehensive': True}]}
explained.rows[["program", "status", "premium", "annual_premium"]]
#   program     status  premium  annual_premium
# 0       a  succeeded    450.0             NaN
# 1       b  succeeded      NaN           570.0
```

Each input is a value the carrier's `prepare` built, in the carrier's own terms. Compare each
value with the book's facts for that submission. `explained.book` holds those facts.

## Find the unrated rows

Rate the full book, and list each row with a status other than `succeeded`. Show each of these
rows in the exhibit, with its reason. [Results](/relativities/reference/results) lists each status.

```python theme={null}
results = market.rate(book)
results.loc[results["status"] != "succeeded", ["submission_id", "program", "status", "reason_code"]]
#   submission_id program          status       reason_code
# 5            S3       b         refused  MILES_OVER_LIMIT
# 6            S4       a  invalid_inputs       INPUT-RANGE
# 7            S4       b         refused  MILES_OVER_LIMIT
# 9            S5       b  mapping_failed    PREPARE_FAILED
```

* S3 and S4 drive more than 25,000 miles a year, so carrier B refuses them with its own code.
* S4 drives 31,000 miles a year, above carrier A's limit of 30,000, so the status is `invalid_inputs`.
* S5's driver D6 has a blank first licensed date, so carrier B's `prepare` raises for S5 alone. `reason` names the function and the exception, and carrier B rates the other submissions.

## Check which coverages each premium includes

Expand carrier A's `vehicles` outcome, and match each entry to the book's vehicle at the same
position:

```python theme={null}
returned = results.query("program == 'a' and status == 'succeeded'")[["submission_id", "vehicles"]]
returned = returned.explode("vehicles", ignore_index=True)
returned["position"] = returned.groupby("submission_id").cumcount()
vehicles = book.vehicles.assign(position=lambda frame: frame.groupby("submission_id").cumcount())
amounts = returned.merge(vehicles[["submission_id", "position", "vehicle_id"]])
amounts = amounts.join(pd.json_normalize(amounts["vehicles"].tolist()))
amounts.query("submission_id in ['S1', 'S6']")[["submission_id", "vehicle_id", "premium.liability", "premium.collision"]]
#   submission_id vehicle_id  premium.liability  premium.collision
# 0            S1         V1                300                150
# 5            S6         V7                240                300
# 6            S6         V8                300                  0
# 7            S6         V9                300                150
```

Carrier A prices liability and collision on each vehicle. A collision premium of \$0 marks a
vehicle that the book leaves at liability only. Carrier B returns one account premium that
includes comprehensive.

## Compare on one basis

1. Choose the coverages that both carriers price. Here, the basis is liability only.
2. Keep only those rows in `vehicle_coverages`, and rate the new book with the market.
3. Put each premium on one term. Double carrier A's six-month premium, and keep carrier B's annual premium.
4. Pivot by submission and program, as the first example on this page shows.

With more carriers, name each carrier's premium column and term once:

```python theme={null}
outcome = {"a": "premium", "b": "annual_premium"}
months = {"a": 6, "b": 12}
compared["annual"] = pd.concat([compared.loc[compared["program"] == p, c] for p, c in outcome.items()])
compared["annual"] = compared["annual"] * 12 / compared["program"].map(months)
compared.pivot(index="submission_id", columns="program", values="annual").loc[["S1", "S2"]]
# program             a       b
# submission_id
# S1              600.0   480.0
# S2             1350.0  1176.0
```

Each carrier's filing states its term. Keep each `NaN` row in the exhibit with its reason.

Doubling a six-month premium is an illustrative annualization. For a market study, also align limits, deductibles, policy fees, and effective dates.
Use the appropriate term conversion for your analysis and state it in the exhibit.

## Build a competitive exhibit by segment

Join your own analysis dimensions to the aligned results. This example uses region and counts every submission before selecting matched premiums:

```python theme={null}
population = book.submissions[["submission_id", "household_id"]].merge(
    book.households[["household_id", "region"]], on="household_id", validate="many_to_one"
)
exhibit = population.merge(annual.reset_index(), on="submission_id", validate="one_to_one")
exhibit["comparable"] = exhibit[["a", "b"]].notna().all(axis=1)
summary = exhibit.groupby("region").agg(
    submissions=("submission_id", "size"), compared=("comparable", "sum")
)
matched = exhibit.loc[exhibit["comparable"]].copy()
totals = matched.groupby("region")[["a", "b"]].sum()
summary["a_above_b_pct"] = (100 * (totals["a"] / totals["b"] - 1)).round(2)
summary
#           submissions  compared  a_above_b_pct
# region
# metro               3         2          17.75
# rural               2         0            NaN
# suburban            1         1          21.74
```

The percentage compares summed premiums for the same matched submissions, with one observation per submission.
The rural segment has zero matched risks and stays visible. Its competitive premium gap remains undefined.
Keep program statuses and reasons from `compared` with this summary; three of six submissions have premiums from both programs.

Use your own dimensions for further cuts, such as vehicle count, driver experience, territory, or distribution channel.
Keep interactions visible where a broad average could conceal them. [Prepare a book](/relativities/how-to/prepare-a-book) shows how to retain custom dimensions.

For a weighted portfolio exhibit, apply the same weights to each program on the matched population.
State whether your statistic compares total premiums, averages individual percentage gaps, or counts how often a program has the lower premium.
Each statistic answers a different question. Treat rating exceptions as a separate population with explicit counts and reasons.

## Define the eligibility basis

Decide whether the study measures price position, underwriting appetite, or both.
`status == "succeeded"` establishes a calculated premium. Underwriting appetite needs its own evidence, such as the carrier's guidelines.
Join that evidence to the results in pandas, by `submission_id` and `program`.

## Add one carrier's own estimate

To give one carrier a fact that the book leaves blank, give that carrier a new `prepare`. Carrier
B estimates a blank first licensed date as the driver's sixteenth birthday:

```python theme={null}
def carrier_b_estimated(book):
    drivers = book.drivers
    sixteenth = drivers["birth_date"] + pd.DateOffset(years=16)
    drivers["first_licensed_date"] = drivers["first_licensed_date"].fillna(sixteenth)
    return carrier_b(book.with_tables(drivers=drivers))


estimated = Program(market.programs["b"].rater, carrier_b_estimated, schema=ppa.SCHEMA)
estimated.explain(liability, "S5").json_in["operators"]
# [{'years_licensed': 17}]
```

The estimate changes only what carrier B receives. The book keeps D6's date blank, and carrier
A's program is unchanged. Label a comparison that uses the estimate as "carrier B's estimate".

## Rate with the estimate

Put the changed program in a market, and rate the book again:

```python theme={null}
viewed = Market({"a": market.programs["a"], "b": estimated})
results = viewed.rate(liability)
results.query("submission_id in ['S3', 'S4', 'S5']")[
    ["submission_id", "program", "status", "premium", "annual_premium"]
]
#   submission_id program          status  premium  annual_premium
# 4            S3       a       succeeded    300.0             NaN
# 5            S3       b         refused      NaN             NaN
# 6            S4       a  invalid_inputs      NaN             NaN
# 7            S4       b         refused      NaN             NaN
# 8            S5       a       succeeded    240.0             NaN
# 9            S5       b       succeeded      NaN           420.0
```

With its estimate, carrier B rates S5 at \$420 for a twelve-month term.

## Close the raters

```python theme={null}
for program in market.programs.values():
    program.close()
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.