Creación de transparencia en los precios de los planes de salud en la cobertura con Lakehouse


¿Qué es la transparencia de precios y qué desafíos presenta?

En los Estados Unidos, los sistemas de prestación de atención médica y los planes de salud enfrentan nuevos requisitos regulatorios en torno a la transparencia de precios. Los Centros de Servicios de Medicare y Medicaid (CMS) son responsables de hacer cumplir estas regulaciones, con el objetivo de aumentar la transparencia y ayudar a los consumidores a comprender mejor los costos asociados con su atención médica.

La transparencia de precios hospitalarios entró en vigencia por primera vez el 1 de enero de 2021 y requiere que cada hospital brinde información de precios clara y accesible en línea, como un archivo integral legible por máquina con todos los artículos y servicios, y como una muestra de servicios comprables en un formato amigable para el consumidor. formato.1 En la práctica, las principales deficiencias, como la tergiversación de los costos totales de los servicios hospitalarios y los requisitos ambiguos sobre el formato, el significado y la disponibilidad de los datos, constituyen barreras para los consumidores.

La transparencia de precios del plan de salud (también conocida como Transparencia en la Cobertura), entró en vigencia por primera vez el 1 de julio de 2022 y requiere que los planes de salud publiquen información sobre los artículos y servicios cubiertos. Las reglamentaciones están destinadas, al menos en parte, a mejorar la capacidad de los consumidores para comprar la atención médica que mejor satisfaga sus necesidades.2

Las organizaciones enfrentan desafíos con este mandato tanto para compartir datos según las regulaciones como para consumir datos con fines comparativos. Algunos desafíos básicos para los pagadores que publican datos incluyen la ambigüedad en torno a los requisitos de CMS, la necesidad de traer conjuntos de datos dispares a través de una organización (patrocinadores de planes, redes de proveedores, negociación de tarifas) y el gran volumen de datos que produce una organización. También prevalecen los mismos desafíos para el consumo por parte de los clientes: trabajar con grandes volúmenes de datos, consumir formatos de datos semiestructurados y conformar conjuntos de datos a través de la curación y el análisis.

¿Qué papel juega Databricks?

Databricks es una plataforma construida para la escalabilidad sobre estándares de código abierto. Esto se aplica a la gran cantidad de datos que se producen y la naturaleza semiestructurada del formato de transparencia de precios de CMS. Aprovechando los estándares abiertos de la plataforma, podemos ampliar la funcionalidad y crear sin problemas soluciones que no hace mucho parecían inverosímiles.

Cerrando la brecha para el análisis:

Un enfoque de transmisión de Apache Spark™ personalizado

En pocas palabras, queremos poder leer grandes datos de archivos legibles por máquina (MRF) de Transparencia de precios de forma nativa en Apache Spark (capacidad de procesamiento distribuido central a escalable) y comenzar a usar SQL (posiblemente la mejor capacidad para el análisis generalizado) para comparar tarifas. Nuestro enfoque a continuación finalmente producirá el resultado:


# learn the file
df = (
 spark
  .readStream
  .format("payer-mrf")
  .load("<json file>")
)
# save to desk(s)...

---Begin evaluation on DBSQL 
SELECT payer_name, billing_code, billing_code_type, ... FROM ...

Para comprender el desafío de realizar un análisis de estos datos proporcionados, necesitamos saber un poco más sobre cómo se proporcionan estos datos. CMS proporciona pautas básicas3 para que los datos se ajusten a, y en su mayor parte, los planes de salud han estado publicando la información en formato JSON, que es un tipo de estructura clave/valor.

El desafío se presenta debido a una combinación de dos factores: el primero es que estos MRF pueden ser muy grandes. Un archivo comprimido de 4GB puede descomprimirse a 150GB. Esto por sí solo no presenta un desafío insuperable. El issue que hace que el tamaño sea relevante es que la estructura JSON exigida por CMS permite la creación de un solo objeto JSON. Hasta ahora, muchos analizadores JSON funcionan leyendo el objeto JSON completo. Incluso los analizadores más rápidos como simdjson (un analizador GPU JSON) requieren que todo el objeto encaje perfectamente en la memoria. Por eso buscamos otra solución.

Nuestro enfoque es transmitir el objeto JSON y analizarlo sobre la marcha. De esta manera, evitamos la necesidad de colocar todo el objeto en la memoria y existen varios analizadores JSON que hacen precisamente esto. Sin embargo, este enfoque todavía no ofrece una estructura consumible para el análisis. Convertir un archivo JSON de 150 GB en 500 archivos JSON pequeños en una máquina native todavía deja mucho que desear.

Aquí es donde vemos que Spark Structured Streaming tiene una clara ventaja. En primer lugar, está completamente integrado con las capacidades distribuidas y de alto rendimiento de Spark. En segundo lugar, tiene capacidades de reinicio manejables, guardando y asignando compensaciones para que un proceso pueda reiniciarse correctamente. En tercer lugar, una de las formas más eficientes de realizar análisis es usar Databricks SQL (DBSQL). Spark Streaming nos permite aterrizar estos grandes objetos JSON de una manera que podemos comenzar a aprovechar DBSQL de inmediato. No más transformaciones pesadas. No se necesitan habilidades importantes de ingeniería de datos o ingeniería de software program.

¿Entonces, cómo funciona? Primero, debemos comprender los contratos de transmisión estructurada de Spark. Una vez que comprendemos las expectativas de Spark, podemos desarrollar un enfoque para dividir archivos JSON grandes y muchos archivos JSON a escala.

Implementación de una fuente Spark Streaming personalizada

Nota: Para el ingeniero de datos, el ingeniero de software program o aquellos que tienen curiosidad, profundizaremos en algunos aspectos internos de Spark con Scala en esta sección.

¿Qué es?

Para que sea un Spark Streaming personalizado debemos implementar dos clases, StreamSourceProvider and Supply. El primero le cube a Spark el proveedor de la fuente personalizada. El segundo describe cómo Spark interactúa con la fuente del cliente. Nos centraremos en este último en este artículo, ya que proporciona la mayor parte de nuestra implementación.

Veamos brevemente lo que significa extender fuente:


override def schema: StructType = ???
override def getBatch(begin: Possibility(Offset), finish: Offset): DataFrame = ???
override def getOffset: Possibility(Offset) = ???
override def commit(finish: Offset): Unit = ???
override def cease(): Unit = ???

Sin entrar en demasiados detalles, podemos decir que Spark hará algunas cosas importantes:

  1. Spark quiere saber el esquema resultante de nuestra fuente personalizada
  2. Spark querrá saber qué “compensación” está disponible actualmente en la transmisión (más sobre esto más adelante)
  3. Spark solicitará datos a través de compensaciones de inicio/fin a través del método getBatch(). Seremos responsables de proporcionar el DataFrame (en realidad, un plan de ejecución para producir un DataFrame) representado por estas compensaciones
  4. Spark “comprometerá” periódicamente estas compensaciones en la plataforma (lo que significa que no necesitamos mantener esta información en el futuro)

Una fuente de transmisión de archivos JSON

Pensando en esto dentro del contexto de nuestro objetivo de dividir archivos JSON masivos, queremos proporcionar subconjuntos del archivo JSON para Spark. Dividiremos la responsabilidad de “proporcionar” compensaciones y “consumir” compensaciones en subprocesos separados y proporcionaremos acceso síncrono a una estructura de datos compartida entre subprocesos.

Proporcionar y consumir compensaciones se dividen en subprocesos separados
Proporcionar y consumir compensaciones se dividen en subprocesos separados

Debido a que esta estructura de datos compartida es mutable, queremos que sea lo más ligera posible. Simplemente, podemos proporcionar una representación ligera de un subconjunto de un archivo JSON como ubicaciones de inicio y fin. El propósito de “headerKey” es representar la clave JSON para una lista grande que puede dividirse (en caso de que estemos en el medio de esta lista y la dividamos). Esto proporcionará una mayor legibilidad en nuestras divisiones JSON resultantes.


case class JsonPartition(begin: Lengthy, finish: Lengthy,  headerKey: String = "") extends Partition{ }

Esta estructura de datos contendrá un solo “desplazamiento” que consumirá la chispa. Tenga en cuenta que esta representación tiene múltiples propósitos como una forma de representar filas y particiones en nuestros datos.

Materialización de compensaciones en Spark

La clase de caso JsonPartition anterior proporciona una compensación de nuestro archivo JSON en la secuencia. Para hacer uso de esta información, debemos decirle a Spark cómo interpretar esto para producir una fila interna.


personal class JsonMRFRDD(
  sc: SparkContext,
  partitions: Array(JsonPartition),
  fileName: Path)
    extends RDD(InternalRow)(sc, Nil) {
override def compute(thePart: Partition, context: TaskContext): Iterator(InternalRow) =  ???
}

Aquí es donde entra en juego el método de cálculo. Este método de nuestra clase tiene información sobre (1) la JsonPartition con los desplazamientos de inicio/fin del archivo y la cadena headerKey, así como (2) el nombre del archivo que estamos analizando desde la instanciación de la clase.

Dada esta información, es bastante sencillo crear una fila en la función de cálculo. Flojamente se ve algo como esto:


override def compute(thePart: Partition, context: TaskContext): Iterator(InternalRow) =  { 
  //Open the file and learn from the begin location
  val in = FileSystem.open(fileName)
  val half = thePart.asInstanceOf(JsonPartition)
  in.search(half.begin)
  //Eat the file between the begin/finish areas
  var buffer = new Array(Byte)(( half.finish - half.begin + 1).toInt)
  ByteStreams.readFully(in, buffer)
  in.shut 

  //Inner Row of "filename", "headerKey", and "JSON Information" 
  InternalRow(
    UTF8String.fromString(fileName.getName),   
    UTF8String.fromString(half.headerKey), 
    UTF8String.fromBytes(buffer)
  )
}

Completando este círculo, implementamos nuestro método getBatch() de la siguiente manera:
(1) Filtrado de la secuencia JsonPartition solicitada por Spark en los desplazamientos de inicio/fin


override def getBatch(begin: Possibility(Offset), finish: Offset): DataFrame =  this.synchronized {
    val s = begin.flatMap({ off =>
      off match {
        case lo: LongOffset => Some(lo)
        case _ => None
      }
    }).getOrElse(LongOffset(-1)).offset+1

    val e = (finish match {
      case lo: LongOffset => lo
      case _ => LongOffset(-1)
    }).offset+1

    val elements = batches.par
     .filter{ case (_, idx) => idx >= s && idx <= e}
     .zipWithIndex
     .map({ 
        case (v, idx2) => 
        new JsonPartition(v._1.begin, v._1.finish, v._1.headerKey, idx2)}).toArray

(2) Finalmente, crear un nuevo plan de cálculo () para que Spark lo interprete en un DataFrame


val catalystRows = new JsonMRFRDD(
      sqlContext.sparkContext,
      elements,
      fileName
    )

  val logicalPlan = LogicalRDD(
      JsonMRFSource.schemaAttributes,
      catalystRows,
      isStreaming = true)(sqlContext.sparkSession)

  val qe = sqlContext.sparkSession.sessionState.executePlan(logicalPlan)
  qe.assertAnalyzed()
  new Dataset(sqlContext.sparkSession, 
      logicalPlan, 
      RowEncoder(qe.analyzed.schema))
}

Análisis JSON

En lo que respecta a Spark Streaming

En cuanto al análisis de JSON, esto es un poco más sencillo. Abordamos este problema creando la clase ByteParser.scala que es responsable de iterar a través de un Array (búfer) de bytes para encontrar información importante como “buscar el siguiente elemento del array” o “saltar espacios en blanco y comas”.

Hay una separación de funciones entre el programa donde ByteParse.scala sirve como métodos puramente funcionales que brindan reutilización y evitan la mutabilidad y el estado international.

El programa que llama es el subproceso creado en JsonMRFSource y es responsable de leer continuamente el archivo, encontrar puntos de inicio y remaining lógicos en un búfer de matriz mientras maneja los casos extremos, y tiene el efecto secundario de pasar esta información a Spark.

¿Cómo se divide funcionalmente un archivo JSON?

Partimos de la premisa de representar datos en un formato divisible sin perder ningún significado de la estructura authentic. El caso trivial es que un par clave/valor se puede dividir y unir sin perder el significado.

Unión de JSON con claves distintas
Unión de JSON con claves distintas

El caso no trivial es que los datos de la clave “en_red” y “referencias_proveedor” a menudo contienen la mayor parte de la información para MRF. Por lo tanto, no es suficiente simplemente dividir en pares clave/valor. Teniendo en cuenta que cada una de estas claves tiene una estructura de datos de valor de matriz, dividimos aún más las matrices.

Agregue matrices JSON junto con la misma clave
Agregue matrices JSON junto con la misma clave

Con este enfoque dividido, nuestro conjunto de datos resultante de la fuente readStream personalizada contiene 3 campos de salida: nombre_archivo, clave_encabezado, y json_carga útil. Columna Nombre del archivo persiste la información necesaria del caso trivial y encabezado_clave información persistente necesaria del caso no trivial.

p.ej ,

Analizando JSON en formato consultable
Analizando JSON en formato consultable

Cumplir con los mandatos de informes de CMS 2023/2024 con DBSQL
Un ejemplo de cómo usar el streamer personalizado se encuentra en un cuaderno de demostración donde descargamos, analizamos y ejecutamos algunos comandos SQL simples que dividen los datos JSON anidados en tablas separadas.

La consulta remaining de la demostración toma un código de práctica y procedimiento del proveedor como parámetros y proporciona una comparación easy de precios entre todos los médicos en la práctica. Algunos ejemplos de los siguientes pasos para hacer que el resultado sea amigable para el consumidor podrían incluir una lista de nombres de proveedores y especialidades mediante la combinación de datos públicos de NPPES, crear una lista de selección de procedimientos a partir de la búsqueda de palabras clave en la descripción y superponer con una herramienta de interfaz de usuario (RShiny, Tableau, and so on.) .

El valor de la transparencia

La visión del mandato de transparencia de precios de CMS es beneficiar a los consumidores a través de una visión transparente y holística de su experiencia de compra de atención médica antes de poner un pie en la oficina de un proveedor. Sin embargo, los obstáculos para lograr este objetivo van más allá de aprovechar los datos MRF publicados a escala.

La codificación de atención médica es extremadamente compleja y el consumidor promedio está lejos de poder discernir esta información sin una experiencia altamente seleccionada. Para una visita a un proveedor determinado, puede haber docenas de códigos facturados, junto con modificadores, así como consideraciones por cosas como complicaciones inesperadas que ocurren. Todo esto para decir que interpretar con precisión un precio sigue siendo un desafío para la mayoría de los consumidores.

Para complicar la interpretación del precio, es possible que haya situaciones en el cuidado de la salud en las que una compensación en el precio corresponda a una compensación en la calidad. Si bien la calidad no se aborda en la transparencia de precios, existen otras herramientas, como las calificaciones STARS de Medicare, que ayudan a cuantificar una calificación de calidad.

Un efecto seguro de las regulaciones es una mayor visibilidad de la dinámica competitiva en el mercado. Los planes de salud ahora tienen acceso a información competitiva crítica que antes no estaba disponible, a saber, la crimson de proveedores y los precios negociados. Se espera que esto afecte las configuraciones de la crimson y las negociaciones de tarifas y, con toda probabilidad, reducirá los valores atípicos de los precios.

Producir valor a partir de la transparencia requiere herramientas analíticas y procesamiento de datos escalable. Al ser una plataforma basada en estándares abiertos y un procesamiento de alto rendimiento, Databricks es especialmente adecuado para ayudar a las organizaciones de atención médica a resolver problemas en un entorno cada vez más complejo.

1https://www.cms.gov/hospital-price-transparency
2https://www.cms.gov/healthplan-price-transparency
3https://github.com/CMSgov/price-transparency-guide/tree/grasp/schemas

Related Articles

Día 7: SQL avanzado para ciencia de datos | de Sunita Rawat | ene, 2023

Hasta ahora, este es el séptimo weblog en el viaje...

Comments

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Same Category

spot_img

Stay in touch!

Follow our Instagram