All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tri.covid19.reports.HotspotInfo.kt Maven / Gradle / Ivy

The newest version!
/*-
 * #%L
 * coda-data
 * --
 * Copyright (C) 2020 - 2022 Elisha Peterson
 * --
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package tri.covid19.reports

import tri.area.Lookup
import tri.covid19.CovidRiskLevel
import tri.covid19.risk_DoublingTime
import tri.covid19.risk_PerCapitaDeathsPerDay
import tri.timeseries.*
import tri.timeseries.analytics.ExtremeInfo
import tri.timeseries.analytics.MinMaxFinder
import tri.util.minus
import java.time.LocalDate
import java.util.*
import kotlin.math.absoluteValue
import kotlin.math.sign

/** Aggregates information about a single hotspot associated with a region. */
data class HotspotInfo(var areaId: String, var metric: String, var start: LocalDate, var values: List, val averageDays: Int = 7) {

    constructor(series: TimeSeries): this(series.areaId, series.metric, series.start, series.values)

    val area = Lookup.areaOrNull(areaId)!!

    val deltas = values.deltas()
    val deltaAverages = deltas.movingAverage(averageDays)
    val doublings = values.movingAverage(averageDays).doublingTimes()
    val doublings14 = values.movingAverage(averageDays).doublingTimes(sinceDaysAgo = 14)
    val doublings28 = values.movingAverage(averageDays).doublingTimes(sinceDaysAgo = 28)

    val value
        get() = values.last()
    val valuePerCapita
        get() = population?.let { value/it * 1E5 }

    val dailyChange
        get() = deltaAverages.lastOrNull()
    val dailyChange7
        get() = deltaAverages.lastOrNull()?.times(7)
    val dailyChange28
        get() = deltas.movingAverage(28).lastOrNull()?.times(28)
    val percentInLast7
        get() = dailyChange7?.div(value)
    val percentInLast7Of28
        get() = dailyChange7.divideOrNull(dailyChange28)
    val doublingTimeDays
        get() = doublings.lastOrNull()
    val doublingTimeDays14
        get() = doublings14.lastOrNull()
    val doublingTimeDays28
        get() = doublings28.lastOrNull()
    val doublingTimeDaysRatio
        get() = doublingTimeDays?.divideOrNull(doublingTimeDays28)
    val severityByChange
        get() = dailyChange?.let { risk_PerCapitaDeathsPerDay(it) } ?: CovidRiskLevel.MINOR
    val severityByDoubling
        get() = doublingTimeDays?.let { risk_DoublingTime(it) } ?: CovidRiskLevel.MINOR
    val totalSeverity
        get() = severityByChange.level + severityByDoubling.level

    val perCapitaPop
        get() = population?.toDouble() divideOrNull 1E5

    val dailyChangePerCapita
        get() = dailyChange divideOrNull perCapitaPop
    val dailyChange7PerCapita
        get() = dailyChange7 divideOrNull perCapitaPop
    val dailyChange28PerCapita
        get() = dailyChange28 divideOrNull perCapitaPop

    val peak7
        get() = values.deltas().movingAverage(7).maxOrNull()?.times(7) ?: 0.0
    val peak7PerCapita
        get() = peak7 divideOrNull perCapitaPop
    val peak7Date
        get() = values.deltas().movingAverage(7).withIndex().maxByOrNull { it.value }?.index?.let { start.plusDays(it + 7L) }
    val peak14
        get() = values.deltas().movingAverage(14).maxOrNull()?.times(14) ?: 0.0
    val peak14PerCapita
        get() = peak14 divideOrNull perCapitaPop
    val peak14Date
        get() = values.deltas().movingAverage(14).withIndex().maxByOrNull { it.value }?.index?.let { start.plusDays(it + 7L) }

    val regionId
        get() = area.id
    val fips
        get() = area.fips
    val population
        get() = area.population

    private val currentTrend
        get() = MinMaxFinder(10).invoke(TimeSeries("", areaId, "", "", false, 0.0, LocalDate.now(), deltas)
                .restrictNumberOfStartingZerosTo(1).movingAverage(7))
                .let { CurrentTrend(it.extrema) }

    val trendDays
        get() = currentTrend.daysSigned
    val changeSinceTrendExtremum
        get() = currentTrend.percentChangeSinceExtremum

    val threeDayPercentChange
        get() = deltas.percentIncrease(4..6, 1..3)
    val sevenDayPercentChange
        get() = deltas.percentIncrease(8..14, 1..7)
    val threeSevenPercentRatio
        get() = threeDayPercentChange divideOrNull sevenDayPercentChange

    private fun List.percentIncrease(bottom: IntRange, top: IntRange): Double? {
        if (top.last > size || bottom.last > size) {
            return null
        }
        val first = subList(maxOf(size - bottom.last - 1, 0), maxOf(size - bottom.first, 0)).average()
        val second = subList(maxOf(size - top.last - 1, 0), maxOf(size - top.first, 0)).average()
        return (second - first)/first
    }
}

private infix fun Double?.divideOrNull(y: Double?) = when {
    this == null || y == null -> null
    else -> this/y
}

/** Compute percentage change from this value to the provided value. */
fun Double.percentChangeTo(count: Double) = (count - this) / this

private class CurrentTrend(map: SortedMap) {
    val curValue by lazy { map.values.last().value }
    val curDate by lazy { map.keys.last()
    }
    val anchorDate by lazy {
        map.keys.reversed().firstOrNull { curDate.minus(it) >= 14 ||
            curDate.minus(it) >= 7 && map[it]!!.value.percentChangeTo(curValue).absoluteValue >= .1 ||
            map[it]!!.value.percentChangeTo(curValue).absoluteValue >= .2 } ?: map.keys.first()
    }
    val anchorValue by lazy { map[anchorDate]!!.value }
    val daysSigned by lazy { ((curValue - anchorValue).sign * curDate.minus(anchorDate)).toInt() }
    val percentChangeSinceExtremum by lazy { anchorValue.percentChangeTo(curValue) }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy