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
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
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 ]
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
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
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
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
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
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