Skip to content

Commit

Permalink
add new stats endpoitn
Browse files Browse the repository at this point in the history
  • Loading branch information
tianj7 committed Dec 10, 2024
1 parent ef8a3d2 commit 202362c
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 0 deletions.
48 changes: 48 additions & 0 deletions controllers/cohortdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,54 @@ func (u CohortDataController) RetrieveHistogramForCohortIdAndConceptId(c *gin.Co
c.JSON(http.StatusOK, gin.H{"bins": histogramData})
}

func (u CohortDataController) RetrieveStatsForCohortIdAndConceptId(c *gin.Context) {
sourceIdStr := c.Param("sourceid")
log.Printf("Querying source: %s", sourceIdStr)
cohortIdStr := c.Param("cohortid")
log.Printf("Querying cohort for cohort definition id: %s", cohortIdStr)
conceptIdStr := c.Param("conceptid")
if sourceIdStr == "" || cohortIdStr == "" || conceptIdStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"message": "bad request"})
c.Abort()
return
}

filterConceptIds, cohortPairs, err := utils.ParseConceptIdsAndDichotomousDefs(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "Error parsing request body for prefixed concept ids", "error": err.Error()})
c.Abort()
return
}

sourceId, _ := strconv.Atoi(sourceIdStr)
cohortId, _ := strconv.Atoi(cohortIdStr)
conceptId, _ := strconv.ParseInt(conceptIdStr, 10, 64)

validAccessRequest := u.teamProjectAuthz.TeamProjectValidation(c, []int{cohortId}, cohortPairs)
if !validAccessRequest {
log.Printf("Error: invalid request")
c.JSON(http.StatusForbidden, gin.H{"message": "access denied"})
c.Abort()
return
}

cohortData, err := u.cohortDataModel.RetrieveHistogramDataBySourceIdAndCohortIdAndConceptIdsAndCohortPairs(sourceId, cohortId, conceptId, filterConceptIds, cohortPairs)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "Error retrieving concept details", "error": err.Error()})
c.Abort()
return
}

conceptValues := []float64{}
for _, personData := range cohortData {
conceptValues = append(conceptValues, float64(*personData.ConceptValueAsNumber))
}

statsData := utils.GenerateStatsData(cohortId, conceptId, conceptValues)

c.JSON(http.StatusOK, gin.H{"statsData": statsData})
}

func (u CohortDataController) RetrieveDataBySourceIdAndCohortIdAndVariables(c *gin.Context) {
// TODO - add some validation to ensure that only calls from Argo are allowed through since it outputs FULL data?

Expand Down
3 changes: 3 additions & 0 deletions server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ func NewRouter() *gin.Engine {
// full data endpoints:
authorized.POST("/cohort-data/by-source-id/:sourceid/by-cohort-definition-id/:cohortid", cohortData.RetrieveDataBySourceIdAndCohortIdAndVariables)

// cohort data statistics
authorized.POST("/cohort-data/stats/by-source-id/:sourceid/by-cohort-definition-id/:cohortid/by-concept-id/:conceptid", cohortData.RetrieveStatsForCohortIdAndConceptId)

// histogram endpoint
authorized.POST("/histogram/by-source-id/:sourceid/by-cohort-definition-id/:cohortid/by-histogram-concept-id/:histogramid", cohortData.RetrieveHistogramForCohortIdAndConceptId)

Expand Down
50 changes: 50 additions & 0 deletions tests/controllers_tests/controllers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1200,3 +1200,53 @@ func TestGenerateDataDictionary(t *testing.T) {
}

}

func TestRetrieveStatsForCohortIdAndConceptIdWithWrongParams(t *testing.T) {
setUp(t)
requestContext := new(gin.Context)
requestContext.Params = append(requestContext.Params, gin.Param{Key: "sourceid", Value: strconv.Itoa(tests.GetTestSourceId())})
requestContext.Params = append(requestContext.Params, gin.Param{Key: "cohortid", Value: "4"})
requestContext.Writer = new(tests.CustomResponseWriter)
requestContext.Request = new(http.Request)
requestBody := "{\"variables\":[{\"variable_type\": \"custom_dichotomous\", \"cohort_ids\": [1, 3]}]}"
requestContext.Request.Body = io.NopCloser(strings.NewReader(requestBody))
//requestContext.Writer = new(tests.CustomResponseWriter)
cohortDataController.RetrieveStatsForCohortIdAndConceptId(requestContext)
// Params above are wrong, so request should abort:
if !requestContext.IsAborted() {
t.Errorf("should have aborted")
}
}

func TestRetrieveStatsForCohortIdAndConceptIdWithCorrectParams(t *testing.T) {
setUp(t)
requestContext := new(gin.Context)
requestContext.Params = append(requestContext.Params, gin.Param{Key: "sourceid", Value: strconv.Itoa(tests.GetTestSourceId())})
requestContext.Params = append(requestContext.Params, gin.Param{Key: "cohortid", Value: "4"})
requestContext.Params = append(requestContext.Params, gin.Param{Key: "conceptid", Value: "2000006885"})
requestContext.Writer = new(tests.CustomResponseWriter)
requestContext.Request = new(http.Request)
requestBody := "{\"variables\":[{\"variable_type\": \"concept\", \"concept_id\": 2000000324},{\"variable_type\": \"custom_dichotomous\", \"cohort_ids\": [1, 3]}]}"
requestContext.Request.Body = io.NopCloser(strings.NewReader(requestBody))
cohortDataController.RetrieveStatsForCohortIdAndConceptId(requestContext)
// Params above are correct, so request should NOT abort:
if requestContext.IsAborted() {
t.Errorf("Did not expect this request to abort")
}
result := requestContext.Writer.(*tests.CustomResponseWriter)
if !strings.Contains(result.CustomResponseWriterOut, "statsData") {
t.Errorf("Expected output starting with 'statsData,...'")
}

// the same request should fail if the teamProject authorization fails:
requestContext.Request.Body = io.NopCloser(strings.NewReader(requestBody))
cohortDataControllerWithFailingTeamProjectAuthz.RetrieveStatsForCohortIdAndConceptId(requestContext)
result = requestContext.Writer.(*tests.CustomResponseWriter)
// expect error:
if !strings.Contains(result.CustomResponseWriterOut, "access denied") {
t.Errorf("Expected 'access denied' as result")
}
if !requestContext.IsAborted() {
t.Errorf("Expected request to be aborted")
}
}
16 changes: 16 additions & 0 deletions tests/utils_tests/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,19 @@ func TestSubtract(t *testing.T) {
t.Errorf("Expected [] but found %v", result)
}
}

func TestGenerateStatsData(t *testing.T) {
setUp(t)

var emptyData = []float64{}
result := utils.GenerateStatsData(1, 1, emptyData)
if result != nil {
t.Errorf("Expected a nil result for an empty data set")
}

var expectedResult = &utils.ConceptStats{CohortId: 1, ConceptId: 1, NumberOfPeople: 11, Min: 6.0, Max: 49.0, Avg: 33.18181818181818, Sd: 15.134657288477642}
result = utils.GenerateStatsData(1, 1, testData)
if !reflect.DeepEqual(expectedResult, result) {
t.Errorf("Expected %v but found %v", expectedResult, result)
}
}
44 changes: 44 additions & 0 deletions utils/stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package utils

import (
"log"

"github.com/montanaflynn/stats"
)

type ConceptStats struct {
CohortId int `json:"cohortId"`
ConceptId int64 `json:"conceptId"`
NumberOfPeople int `json:"personCount"`
Min float64 `json:"min"`
Max float64 `json:"max"`
Avg float64 `json:"avg"`
Sd float64 `json:"sd"`
}

func GenerateStatsData(cohortId int, conceptId int64, conceptValues []float64) *ConceptStats {

if len(conceptValues) == 0 {
log.Printf("Data size is zero. Returning nil.")
return nil
}

result := new(ConceptStats)
result.CohortId = cohortId
result.ConceptId = conceptId
result.NumberOfPeople = len(conceptValues)

minValue, _ := stats.Min(conceptValues)
result.Min = minValue

maxValue, _ := stats.Max(conceptValues)
result.Max = maxValue

meanValue, _ := stats.Mean(conceptValues)
result.Avg = meanValue

sdValue, _ := stats.StandardDeviation(conceptValues)
result.Sd = sdValue

return result
}

0 comments on commit 202362c

Please sign in to comment.