示例#1
0
def get_savings_for_orgs(generic_code, date, org_type, org_ids, min_saving=1):
    """
    Get available savings for the given orgs within a particular class of
    substitutable presentations
    """
    try:
        substitution_set = get_substitution_sets()[generic_code]
    # Gracefully handle being asked for the savings for a code with no
    # substitutions (to which the answer is always: no savings)
    except KeyError:
        return []

    quantities, net_costs = get_quantities_and_net_costs_at_date(
        get_db(), substitution_set, date
    )

    group_by_org = get_row_grouper(org_type)
    quantities_for_orgs = group_by_org.sum(quantities, org_ids)
    # Bail early if none of the orgs have any relevant prescribing
    if not numpy.any(quantities_for_orgs):
        return []
    net_costs_for_orgs = group_by_org.sum(net_costs, org_ids)
    ppu_for_orgs = net_costs_for_orgs / quantities_for_orgs

    target_ppu = get_target_ppu(
        quantities,
        net_costs,
        group_by_org=get_row_grouper(CONFIG_TARGET_PEER_GROUP),
        target_centile=CONFIG_TARGET_CENTILE,
    )
    practice_savings = get_savings(quantities, net_costs, target_ppu)

    savings_for_orgs = group_by_org.sum(practice_savings, org_ids)

    results = [
        {
            "date": date,
            "org_id": org_id,
            "price_per_unit": ppu_for_orgs[offset, 0] / 100,
            "possible_savings": savings_for_orgs[offset, 0] / 100,
            "quantity": quantities_for_orgs[offset, 0],
            "lowest_decile": target_ppu[0] / 100,
            "presentation": substitution_set.id,
            "formulation_swap": substitution_set.formulation_swaps,
            "name": substitution_set.name,
        }
        for offset, org_id in enumerate(org_ids)
        if savings_for_orgs[offset, 0] >= min_saving
    ]

    results.sort(key=lambda i: i["possible_savings"], reverse=True)
    return results
def _get_total_prescribing_entries(bnf_code_prefixes):
    """
    Yields a dict for each date in our data giving the total prescribing values
    across all practices for all presentations matching the supplied BNF code
    prefixes
    """
    db = get_db()
    items_matrix, quantity_matrix, actual_cost_matrix = _get_prescribing_for_codes(
        db, bnf_code_prefixes)
    # If no data at all was found, return early which results in an empty
    # iterator
    if items_matrix is None:
        return
    # This will sum over every practice (whether setting 4 or not) which might
    # not seem like what we want but is what the original API did (it was
    # powered by the `vw__presentation_summary` table which summed over all
    # practice types)
    group_all = get_row_grouper("all_practices")
    items_matrix = group_all.sum(items_matrix)
    quantity_matrix = group_all.sum(quantity_matrix)
    actual_cost_matrix = group_all.sum(actual_cost_matrix)
    # Yield entries for each date (unlike _get_prescribing_entries below we
    # return a value for each date even if it's zero as this is what the
    # original API did)
    for date, col_offset in sorted(db.date_offsets.items()):
        # The grouped matrices only ever have one row (which represents the
        # total over all practices) so we always want row 0 in our index
        index = (0, col_offset)
        yield {
            "items": items_matrix[index],
            "quantity": quantity_matrix[index],
            "actual_cost": round(actual_cost_matrix[index], 2),
            "date": date,
        }
def get_ppu_breakdown(prescribing, org_type, org_id):
    """
    Given a prescribing dict (see `get_prescribing` above) return a breakdown
    of how much of each presentation was prescribed at each price-per-unit by
    the given org.

    Note that we round PPUs to the nearest pence, so that if 10 units were
    prescribed at 9.9p each and 5 units at 10.1p this function will say that 15
    units were prescribed at 10p each.
    """
    group_by_org = get_row_grouper(org_type)
    names = Presentation.names_for_bnf_codes(prescribing.keys())
    presentations = []
    for bnf_code, (quantities, net_costs) in prescribing.items():
        quantities = group_by_org.get_group(quantities, org_id)[:, 0]
        net_costs = group_by_org.get_group(net_costs, org_id)[:, 0]
        ppu = net_costs / quantities
        rounded_ppu = numpy.rint(ppu)
        ppu_values = numpy.unique(rounded_ppu)
        ppu_values = ppu_values[numpy.isfinite(ppu_values)]
        if len(ppu_values):
            presentations.append({
                "name":
                names.get(bnf_code, "{} (unknown)".format(bnf_code)),
                "mean_ppu":
                net_costs.sum() / quantities.sum(),
                "is_generic":
                bnf_code[9:11] == "AA",
                "quantity_at_each_ppu":
                [(ppu_value, quantities[rounded_ppu == ppu_value].sum())
                 for ppu_value in ppu_values],
            })
    presentations.sort(key=lambda i: (i["mean_ppu"], i["name"]))
    return presentations
示例#4
0
def _get_prescribed_quantity_matrix(bnf_code_offsets, date_offsets, org_type, org_id):
    """
    Given a mapping of BNF codes to row offsets and dates to column offsets,
    return a matrix giving the quantity of those presentations prescribed on
    those dates by the specified organisation (given by its type and ID).

    If the dates extend beyond the latest date for which we have prescribing
    data then we just project the last month forwards (e.g. if we only have
    prescriptions up to March but have price concessions up to May then
    we just assume the same quantities as for March were prescribed in April
    and May).
    """
    db = get_db()
    group_by_org = get_row_grouper(org_type)
    shape = (len(bnf_code_offsets), len(date_offsets))
    quantities = numpy.zeros(shape, dtype=numpy.int_)
    # If this organisation is not in the set of available groups (because it
    # has no prescribing data) then return the zero-valued quantity matrix
    if org_id not in group_by_org.offsets:
        return quantities
    # Find the columns corresponding to the dates we're interested in
    columns_selector = _get_date_columns_selector(db.date_offsets, date_offsets)
    prescribing = _get_quantities_for_bnf_codes(db, list(bnf_code_offsets.keys()))
    for bnf_code, quantity in prescribing:
        # Remap the date columns to just the dates we want
        quantity = quantity[columns_selector]
        # Sum the prescribing for the given organisation
        quantity = group_by_org.sum_one_group(quantity, org_id)
        # Write that sum into the quantities matrix at the correct offset for
        # the current BNF code
        row_offset = bnf_code_offsets[bnf_code]
        quantities[row_offset] = quantity
    return quantities
示例#5
0
def _get_practice_codes_for_ccg(ccg_id):
    practices = Practice.objects.filter(ccg_id=ccg_id, setting=4)
    # Remove any practice codes with no associated prescribing
    prescribing_practice_codes = get_row_grouper("practice").offsets.keys()
    return [
        code for code in practices.values_list("code", flat=True)
        if code in prescribing_practice_codes
    ]
示例#6
0
def _get_prescribing_entries(bnf_code_prefixes, orgs, org_type, date=None):
    """
    For each date and organisation, yield a dict giving totals for all
    prescribing matching the supplied BNF code prefixes.

    If a date is supplied then data for just that date is returned, otherwise
    all available dates are returned.
    """
    db = get_db()
    items_matrix, quantity_matrix, actual_cost_matrix = _get_prescribing_for_codes(
        db, bnf_code_prefixes)
    # If no data at all was found, return early which results in an empty
    # iterator
    if items_matrix is None:
        return
    # Group together practice level data to the appropriate organisation level
    group_by_org = get_row_grouper(org_type)
    items_matrix = group_by_org.sum(items_matrix)
    quantity_matrix = group_by_org.sum(quantity_matrix)
    actual_cost_matrix = group_by_org.sum(actual_cost_matrix)
    # `group_by_org.offsets` maps each organisation's primary key to its row
    # offset within the matrices. We pair each organisation with its row
    # offset, ignoring those organisations which aren't in the mapping (which
    # implies that they did not prescribe in this period)
    org_offsets = [(org, group_by_org.offsets[org.pk]) for org in orgs
                   if org.pk in group_by_org.offsets]
    # Pair each date with its column offset (either all available dates or just
    # the specified one)
    if date:
        try:
            date_offsets = [(date, db.date_offsets[date])]
        except KeyError:
            raise BadDate(date)
    else:
        date_offsets = sorted(db.date_offsets.items())
    # Yield entries for each organisation on each date
    for date, col_offset in date_offsets:
        for org, row_offset in org_offsets:
            index = (row_offset, col_offset)
            items = items_matrix[index]
            # Mimicking the behaviour of the existing API, we don't return
            # entries where there was no prescribing
            if items == 0:
                continue
            entry = {
                "items": items,
                "quantity": quantity_matrix[index],
                "actual_cost": round(actual_cost_matrix[index], 2),
                "date": date,
                "row_id": org.pk,
                "row_name": org.name,
            }
            # Practices get some extra attributes in the existing API
            if org_type == "practice":
                entry["ccg"] = org.ccg_id
                entry["setting"] = org.setting
            yield entry
示例#7
0
def get_total_ghost_branded_generic_spending(date, org_type, org_id):
    """
    Get the total spend on generics (by this org and in this month) over and
    above the price set by the Drug Tariff
    """
    db = get_db()
    practice_spending = get_total_ghost_branded_generic_spending_per_practice(
        db, date, PRESENTATIONS_TO_IGNORE, MIN_GHOST_GENERIC_DELTA
    )
    group_by_org = get_row_grouper(org_type)
    return group_by_org.sum_one_group(practice_spending, org_id)[0] / 100
示例#8
0
def get_total_savings_for_org(date, org_type, org_id):
    """
    Get total available savings through presentation switches for the given org
    """
    group_by_org = get_row_grouper(org_type)
    substitution_sets = get_substitution_sets()
    # This only happens during testing where a test case might not have enough
    # different presentations to generate any substitutions. If this is the
    # case then their are, obviously, zero savings available.
    if not substitution_sets:
        return 0.0
    totals = get_total_savings_for_org_type(
        db=get_db(),
        substitution_sets=substitution_sets,
        date=date,
        group_by_org=group_by_org,
        min_saving=CONFIG_MIN_SAVINGS_FOR_ORG_TYPE[org_type],
        practice_group_by_org=get_row_grouper(CONFIG_TARGET_PEER_GROUP),
        target_centile=CONFIG_TARGET_CENTILE,
    )
    offset = group_by_org.offsets[org_id]
    return totals[offset, 0] / 100
def get_mean_ppu(prescribing, org_type, org_id):
    """
    Given a prescribing dict (see `get_prescribing` above) return the mean
    price-per-unit achieved by the given org over all included presentations
    """
    group_by_org = get_row_grouper(org_type)
    total_quantity = 0
    total_net_cost = 0
    for quantities, net_costs in prescribing.values():
        total_quantity += group_by_org.sum_one_group(quantities, org_id)[0]
        total_net_cost += group_by_org.sum_one_group(net_costs, org_id)[0]
    if total_quantity > 0:
        return total_net_cost / total_quantity
    else:
        return None
示例#10
0
def measure_numerators_by_org(request, format=None):
    measure_id = request.query_params.get("measure", None)
    measure = Measure.objects.get(pk=measure_id)
    org_type, org_id = _get_org_type_and_id_from_request(request)
    group_by_org = get_row_grouper(org_type)

    # Nested function which takes a prescribing matrix and returns the total
    # value for the current organisation over the last 3 months (where the
    # current organisation is defined by the `group_by_org` and `org_id`
    # variables)
    def get_total(matrix):
        latest_three_months = matrix[:, -3:]
        values_for_org = group_by_org.sum_one_group(latest_three_months,
                                                    org_id)
        return values_for_org.sum()

    bnf_codes, sort_field = _get_bnf_codes_and_sort_field_for_measure(measure)
    prescribing = _get_prescribing_for_bnf_codes(bnf_codes)
    results = []
    for bnf_code, items_matrix, quantity_matrix, actual_cost_matrix in prescribing:
        items = get_total(items_matrix)
        if items == 0:
            continue
        quantity = get_total(quantity_matrix)
        actual_cost = get_total(actual_cost_matrix)
        results.append({
            "bnf_code": bnf_code,
            "total_items": int(items),
            "quantity": int(quantity),
            # Pence to pounds
            "cost": actual_cost / 100.0,
        })
    # Equivalent to ORDER BY and LIMIT
    results.sort(key=lambda i: i[sort_field], reverse=True)
    results = results[:50]
    # Fetch names after truncating results so we have fewer to look up
    names = Presentation.names_for_bnf_codes([i["bnf_code"] for i in results])
    for item in results:
        # Occasional issues with BNF code updates mean we temporarily can't
        # recognise a BNF code until we get the latest copy of the code mapping
        # file.
        item["presentation_name"] = names.get(item["bnf_code"],
                                              "<Name unavailable>")
    response = Response(results)
    filename = "%s-%s-breakdown.csv" % (measure, org_id)
    if request.accepted_renderer.format == "csv":
        response["content-disposition"] = "attachment; filename=%s" % filename
    return response
def _get_practice_stats_entries(keys, org_type, orgs):
    db = get_db()
    practice_stats = db.query(*_get_query_and_params(keys))
    group_by_org = get_row_grouper(org_type)
    practice_stats = [
        (name, group_by_org.sum(matrix)) for (name, matrix) in practice_stats
    ]
    # `group_by_org.offsets` maps each organisation's primary key to its row
    # offset within the matrices. We pair each organisation with its row
    # offset, ignoring those organisations which aren't in the mapping (which
    # implies that we have no statistics for them)
    org_offsets = [
        (org, group_by_org.offsets[org.pk])
        for org in orgs
        if org.pk in group_by_org.offsets
    ]
    # For the "all_practices" grouping we have no orgs and just a single row
    if org_type == "all_practices":
        org_offsets = [(None, 0)]
    date_offsets = sorted(db.date_offsets.items())
    # Yield entries for each organisation on each date
    for date, col_offset in date_offsets:
        for org, row_offset in org_offsets:
            entry = {"date": date}
            if org is not None:
                entry["row_id"] = org.pk
                entry["row_name"] = org.name
            index = (row_offset, col_offset)
            star_pu = {}
            has_value = False
            for name, matrix in practice_stats:
                value = matrix[index]
                if value != 0:
                    has_value = True
                if name == "nothing":
                    value = 1
                if isinstance(value, float):
                    value = round(value, 2)
                if name.startswith("star_pu."):
                    star_pu[name[8:]] = value
                else:
                    entry[name] = value
            if star_pu:
                entry["star_pu"] = star_pu
            if has_value:
                yield entry
示例#12
0
def get_prescribing_for_orgs(db, bnf_codes, date, org_type, org_ids):
    """
    Get all prescribing for a given set of presentations by the given
    organisation on the given date.

    Results are returned as practice level matrices, but containing only the
    practices in the given organisations. Presentations for which there is no
    relevant prescribing are omitted.

    Yields tuples of the form:

        org_id, bnf_code, quantity_matrix, net_cost_matrix
    """
    group_by_org = get_row_grouper(org_type)
    for bnf_code, quantities, net_costs in get_prescribing(db, bnf_codes, date):
        for org_id in org_ids:
            quantities_for_org = group_by_org.get_group(quantities, org_id)
            if numpy.any(quantities_for_org):
                net_costs_for_org = group_by_org.get_group(net_costs, org_id)
                yield org_id, bnf_code, quantities_for_org, net_costs_for_org
示例#13
0
def price_per_unit(request, format=None):
    """
    Returns price per unit data for presentations and practices or
    CCGs
    """
    entity_code = request.query_params.get("entity_code", "")
    entity_type = request.query_params.get("entity_type", "").lower()
    date = request.query_params.get("date")
    bnf_code = request.query_params.get("bnf_code")
    aggregate = bool(request.query_params.get("aggregate"))
    if not date:
        raise NotValid("You must supply a date")
    if not (entity_code or bnf_code or aggregate):
        raise NotValid(
            "You must supply a value for entity_code or bnf_code, or set the "
            "aggregate flag")
    if not entity_type:
        entity_type = "ccg" if len(entity_code) == 3 else "practice"

    filename = date
    if bnf_code:
        filename += "-%s" % bnf_code
    if entity_code:
        filename += "-%s" % entity_code

    # This not a particularly orthogonal API. Below we're just replicating the
    # logic of the original API which we can think about simplifying later.
    #
    # Handle the special All England case
    if aggregate:
        # If we're not looking at a specific code then we want to aggregate all
        # practices together
        if not bnf_code:
            entity_type = "all_standard_practices"
            entity_codes = [None]
        # Otherwise we want the results over all CCGs
        else:
            entity_type = "ccg"
            entity_codes = get_row_grouper(entity_type).ids
    else:
        # If we don't specify a particular org then we want all orgs of that
        # type
        if not entity_code:
            entity_codes = get_row_grouper(entity_type).ids
        else:
            # When looking at a specific BNF code for a specific CCG we
            # actually want the results over its practices
            if entity_type == "ccg" and bnf_code:
                entity_type = "practice"
                entity_codes = _get_practice_codes_for_ccg(entity_code)
            # Otherwise we should just show the specified org
            else:
                entity_codes = [entity_code]

    if bnf_code:
        results = get_savings_for_orgs(bnf_code, date, entity_type,
                                       entity_codes)
    else:
        results = get_all_savings_for_orgs(date, entity_type, entity_codes)

    # Fetch the names of all the orgs involved and prepare to reformat the
    # response to match the old API
    if entity_type == "practice":
        org_id_field = "practice"
        org_name_field = "practice_name"
        org_names = {
            code: nhs_titlecase(name)
            for (code, name) in Practice.objects.filter(
                code__in=entity_codes).values_list("code", "name")
        }
    elif entity_type == "ccg":
        org_id_field = "pct"
        org_name_field = "pct_name"
        org_names = {
            code: nhs_titlecase(name)
            for (code, name) in PCT.objects.filter(
                code__in=entity_codes).values_list("code", "name")
        }
    elif entity_type == "all_standard_practices":
        org_id_field = "pct"
        org_name_field = "pct_name"
        org_names = {None: "NHS England"}
    else:
        raise ValueError(entity_type)

    # All BNF codes which had a price concession that month
    concession_codes = set(_get_concession_bnf_codes(date))

    # Reformat response to match the old API
    for row in results:
        org_id = row.pop("org_id")
        row[org_id_field] = org_id
        row[org_name_field] = org_names[org_id]
        row["price_concession"] = row["presentation"] in concession_codes

    response = Response(results)
    if request.accepted_renderer.format == "csv":
        filename = "%s-ppd.csv" % (filename)
        response["content-disposition"] = "attachment; filename=%s" % filename
    return response