Skip to main content
Toggle menu

Search the website

Changes to Price-Per-Unit Tool

Posted:
Written by:
Categories:

Way back in 2017 we released our Price-Per-Unit tool. We developed this as a new way of identifying where prescribing costs could be reduced without changing the medication the patient was taking. You can read more about how we built the tool on our blog, and our paper in BMJ Open describes what we found from developing it.

The tool works to find the lowest-cost medicine in two different ways:

  1. It calculates the “price-per-unit” (e.g. the cost per tablet, or millilitre) for all the different generic and brand versions of the drug that are available. It then calculates the saving if everyone prescribed at the same price-per-unit as those in the lowest 10%.
  2. It also adds in “formulation swaps”, e.g. if both omeprazole 20mg tablets AND omeprazole 20mg capsules were available, the tool would consider the price-per-unit for brands and generics for both tablets and capsules.

Step 1: no more manual curation

2017 is a long time ago, and we’ve learnt a lot about how to work with prescribing data since then. For example, the original formulation swaps list was created manually in Google Sheets. This requires a lot of time and effort to keep up-to-date, and given the small number of people in the team wasn’t a priority, and so if a new product was released (e.g. atorvastatin 20mg capsules) the tool won’t know to match it to the cheaper tablets.

One thing we’ve learned while developing OpenPrescribing is to reduce reliance on manually listing drugs when making measures. We’ve written before about how we’ve changed measure definitions for areas such as statins and emollients. We wondered if we could do a similar thing to identify formulation swaps.

As a result, we decided it was time to review how the tool is built, in order to make it better for users.

We turned to the Dictionary of Medicines + Devices (dm+d), which has a number of fields which we could use to build an algorithm to identify suitable swaps. We experimented with a few different fields which were available in the dictionary of medicines and devices, and came up with the following criteria for a suitable swap:

  • Must be the same chemical substance (or substances), and not include other substances
  • Must be the same strength (for all chemical substances)
  • Must be the same route (e.g. oral, rectal, injection)
  • Must not have bioavailability issues if prescribed generically or with a different formulation (e.g. exclude phenytoin, where tablet and capsule have different properties)
  • If not oral, must be same “size”, e.g. Morphine 10mg/2ml and 5mg/1ml aren’t equivalent, even though they’re the same strength (5mg/ml)
  • Swaps between “normal release” and “modified-release” aren’t allowed

In addition, following further experimentation, we added the following logic:

  • If an inhaler, we describe the type of device, so that e.g. Metered Dose Inhalers (MDIs) don’t match with Dry Powder Inhalers (DPIs)
  • If it is preservative free, it won’t match with non-preservative free preparations
  • If it’s a skin preparation, it has to be the same type of formulation (e.g. shower gels and skin preparations are not matched).

We use these fields to create a string, and only where this string completely matches other preparations will a formulation swap be matched.

For example the string for Ramipril 10mg tablets (0205051R0AAANAN) is 386872004|10|258684004|NULL|NULL|26643006|1|NULL|NULL|NULL|NULL|NULL

(click to show details of each field).
Field Name Field Description Value Description
ing Chemical ingredient 386872004 Ramipril
basis_strnt How is ingredient strength calculated? 1 Ingredient Substance
strnt_nmrtr_val Strength value numerator 10
strnt_nmrtr_uom Strength value numerator unit 258684004 mg
strnt_dnmtr_val Strength value denominator NULL
strnt_dnmtr_uom Strength value denominator unit NULL
route Drug route 26643006 Oral
pres_stat Prescribing status 1 Valid as a prescribable product
udfs Unit dose form size NULL (Used in non-oral preps, e.g., ampoule size)
formroute.descr Inhaler type NULL (Used in inhalers, e.g., dry-powder)
formroute.descr LIKE '%modified-release%' Is product modified-release NULL
pres_f = TRUE OR nm contains "preservative-free" Is product preservative free NULL pres_f flag isn’t always correct, hence text check
route.descr LIKE '%cutaneous%' Type of formulation NULL Only for skin preparations

NOTE: this doesn’t contain the drug form, i.e. capsule, so it will match to any other generic which matches the whole string. In the case above, we get this:

In the case of ramipril 10mg tablets, the only product from the sample selected that also matches the whole string will be ramipril 10mg capsules.

If you really want to get nerdy, click to see the full SQL that makes this happen:
WITH drugs AS (
    SELECT
        vmp.id,
        nm,
        bnf_code,
        pres_stat,
        droute.route,
        pres_f,
        sug_f,
        CASE WHEN formroute.descr LIKE 'solutioninfusion%' then 'solutioninfusion' -- simplify multiple infusion routes as 'infusion'
        WHEN formroute.descr LIKE 'solutioninjection%' then 'solutioninjection' -- simplify multiple injection routes as 'injection'
        ELSE formroute.descr END AS formroute,
        udfs,
        form.descr AS form,
        STRING_AGG(
            CONCAT(
                COALESCE(COALESCE(CAST (bs_subid AS STRING), (CAST(ing AS STRING))), 'NULL'), '|', -- ingredient code
                COALESCE(CAST(strnt_nmrtr_val AS STRING), 'NULL'), '|', -- strength numerator value
                COALESCE(CAST(strnt_nmrtr_uom AS STRING), 'NULL'), '|',  -- strength numerator unit
                COALESCE(CAST(strnt_dnmtr_val AS STRING), 'NULL'), '|',  -- strength denominator value
                COALESCE(CAST(strnt_dnmtr_uom AS STRING), 'NULL'), '|', -- strength denominator unit
                COALESCE(CAST(droute.route AS STRING), 'NULL'), '|', -- route
                COALESCE(CAST(pres_stat AS STRING), 'NULL'), '|', -- prescribing suitable for primary care value
                COALESCE(CAST(pres_f AS STRING), 'NULL'), '|', -- preservative free flag
                COALESCE(CASE WHEN formroute.descr NOT LIKE '%.oral%' THEN CAST(udfs AS STRING) ELSE 'NULL' END, 'NULL'), '|', -- if not oral meds, then ensure unit doses are the same (e.g. injections), i.e. so that 10mg/5ml isn't offered for 2mg/1ml
                COALESCE(CASE WHEN droute.route = 18679011000001101 THEN CAST(formroute.cd AS STRING) ELSE 'NULL' END, 'NULL'), '|', -- if inhalation (route = 18679011000001101), then include type of device (e.g. dry powder, pressurized) so that device swaps aren't offered
                CASE WHEN pres_f = TRUE OR LOWER(nm) LIKE '%preservative free%' THEN 'pf' ELSE 'NULL' END, '|', -- add 'preservative free' flag so that it doesn't match non pf products.  Some pf eye drops don't have correct flag so have included name
                CASE WHEN LOWER(formroute.descr) LIKE '%modified-release%' THEN 'MR' ELSE 'NULL' END, '|', -- add 'modified release' flag on match string, so that non modified-release preps aren't matched with standard release
                CASE WHEN LOWER(route.descr) LIKE '%cutaneous%' THEN LEFT(formroute.descr, STRPOS(formroute.descr, '.') - 1)  ELSE 'NULL' END -- add type of formulation to cutaneous preps, so that e.g. shower gels aren't offered for skin gels
            ),
            ','
            ORDER BY ing, basis_strnt
        ) AS vpi_string
    FROM
        dmd.vpi AS vpi
    INNER JOIN
        dmd.vmp AS vmp ON vpi.vmp = vmp.id
    INNER JOIN
        dmd.droute AS droute ON vmp.id = droute.vmp
    INNER JOIN
        dmd.route AS route ON route.cd = droute.route
    INNER JOIN
        dmd.ont AS ont ON vmp.id = ONT.vmp
    INNER JOIN
        dmd.ontformroute AS formroute ON ont.form = formroute.cd
    INNER JOIN
        dmd.dform AS dform ON vmp.id = dform.vmp
    INNER JOIN
        dmd.form AS form ON dform.form = form.cd
    WHERE bnf_code IS NOT NULL --make sure all drugs have BNF code
    AND SUBSTR(bnf_code,10,2) = 'AA' --make sure all generic codes
    AND (non_avail != 1 OR non_avail is NULL) -- make sure all drugs are available
    AND strnt_nmrtr_val IS NOT NULL -- make sure all drugs have a strength
    AND pres_stat = 1 -- must be "suitable for prescribing in primary care", e.g. no cautions on switching preparations
    GROUP BY
        vmp.id, nm, bnf_code, pres_stat, pres_f, sug_f, route, formroute, udfs, form
)
SELECT DISTINCT

        drugs_1.bnf_code AS Code,
        drugs_1.nm AS Name,
        drugs_2.bnf_code AS Alternative_code,
        CASE WHEN drugs_1.pres_f IS TRUE THEN concat(drugs_1.form, ' preservative free')
             WHEN drugs_1.sug_f IS TRUE THEN concat(drugs_1.form, ' sugar free')
             ELSE drugs_1.form END AS Formulation,
        drugs_2.nm AS Alternative_name,
        CASE WHEN drugs_2.pres_f IS TRUE THEN concat(drugs_2.form, ' preservative free')
             WHEN drugs_2.sug_f IS TRUE THEN concat(drugs_2.form, ' sugar free') ELSE drugs_2.form END AS Alternative_formulation
    FROM
        drugs AS drugs_1
    INNER JOIN
        drugs AS drugs_2
        ON drugs_1.vpi_string = drugs_2.vpi_string -- only where strings are the same, i.e. same ingredients, strength, and route
    WHERE
        drugs_1.id != drugs_2.id -- so don't get duplication
    AND
        drugs_1.bnf_code != drugs_2.bnf_code -- remove duplication where two different VMP types have same BNF code (e.g. solution/suspension)

We originally used the BNF code map to link brands and generics together, as most drugs have a matching hierarchy (for more details see our blog from 2017). Since then there have been changes to some products, such as some eye drops (e.g. hypromellose) and emollients (e.g. Zerobase), which now appear in the “appliances” part of the BNF code list.

Unlike drugs, appliance codes are an 11 digit non-hierarchical number. For example Zerobase Cream used to have a BNF code of 130201000BBKGCI, meaning it would match to generic Liquid Paraffin Cream 11% (130201000AACICI). It was changed to an appliance code of 21220000214, which doesn’t have a matching generic code.

However the dm+d does give us a map between generic (known as virtual medicinal product) and brand (actual medicinal product), even where they don’t share the same BNF code hierarchy. By using this we were able to match more brands to generics. In the table below, several brands of hypromellose 0.3% eye drops can be identified, despite not having the 1108010F0____AAcode structure:

AMP (brand) name AMP BNF code VMP (generic) name VMP BNF code
Mandanol eye drops 21300000114 Hypromellose 0.3% eye drops 1108010F0AAAAAA
Lumecare Hypromellose 0.3% eye drops 21300000117 Hypromellose 0.3% eye drops 1108010F0AAAAAA
Xailin Hydrate 0.3% eye drops 21300000169 Hypromellose 0.3% eye drops 1108010F0AAAAAA
Ocufresh Hypromellose 0.3% eye drops 21300000188 Hypromellose 0.3% eye drops 1108010F0AAAAAA
Teardew 0.3% eye drops 21300000308 Hypromellose 0.3% eye drops 1108010F0AAAAAA
AaproMel 0.3% eye drops 21300000937 Hypromellose 0.3% eye drops 1108010F0AAAAAA
AacuLose Hypromellose 0.3% eye drops 21300000950 Hypromellose 0.3% eye drops 1108010F0AAAAAA
Ocufresh-C 0.3% eye drops 21300000979 Hypromellose 0.3% eye drops 1108010F0AAAAAA
Puroptics Hypromellose 0.3% eye drops 21300000985 Hypromellose 0.3% eye drops 1108010F0AAAAAA

Step 3: discounts and housekeeping

We’ve also harmonised how we calculate discount deductions for medicines and appliances with our Price Concessions tool, meaning that the savings shown now fully reflect the recent changes to the discount system for pharmacy contractors. You can read more about the changes we made to Price Concessions in our 2024 blog.

Conclusion

As always in a big change like this, testing uncovers some expected issues. One we encountered was that Blood Glucose Testing Strips disappeared from the tool, so we had to deploy a fix to get them back

Hopefully you’ll find all our changes useful, and that the PPU is now better than ever. However, as always, if you find anything that you don’t think quite matches, seems to be missing, or doesn’t work properly please let us know.