// userStatistics provides functions to generate statistics about stored user profiles. // The user chooses the attribute(s), and the gui displays a chart with a button to view the statistics data. // See statisticsGui.go to see the gui code. package userStatistics //TODO: Add the ability to control for confounding variables // Example: Wealth, controlled for age and sex import "seekia/internal/badgerDatabase" import "seekia/internal/helpers" import "seekia/internal/profiles/calculatedAttributes" import "seekia/internal/profiles/profileStorage" import "seekia/internal/profiles/attributeDisplay" import "seekia/internal/translation" import "seekia/internal/statisticsDatum" import "slices" import "strings" import "errors" import "math" //Outputs: // -int: Number of users analyzed in statistics // -[]statisticsDatum.StatisticsDatum: Statistics datums list (sorted, not grouped) // -bool: Grouping performed // -[]statisticsDatum.StatisticsDatum: Grouped datums list // -func(float64)(string, error): Function to format y axis values // -This is used because the values must be passed to the chart code as pure floats, but they must be formatted after to be human readable // -Example: "1000000" -> "1 million" // -error func GetUserStatisticsDatumsLists_BarChart(identityType string, networkType byte, xAxisAttribute string, xAxisIsNumerical bool, formatXAxisValuesFunction func(string)(string, error), xAxisUnknownLabel string, yAxisAttribute string)(int, []statisticsDatum.StatisticsDatum, bool, []statisticsDatum.StatisticsDatum, func(float64)(string, error), error){ isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return 0, nil, false, nil, nil, errors.New("GetUserStatisticsDatumsLists_BarChart called with invalid networkType: " + networkTypeString) } getYAxisRoundingPrecision := func()int{ if (yAxisAttribute == "Number Of Users"){ return 0 } if (yAxisAttribute == "Average Height"){ return 0 } if (yAxisAttribute == "Average Age"){ return 0 } return 1 } yAxisRoundingPrecision := getYAxisRoundingPrecision() //Outputs: // -int: Total analyzed users // -[]statisticsDatum.StatisticsDatum: Statistics datums list // -map[string]int: Response Counts map (X axis attribute response -> Number of y axis responses) // -bool: yAxisIsAverage // -map[string]float64: Response Sums map (X axis attribute response -> all y axis responses summed) (If yAxisIsAverage == true) // -func(float64)(string, error): Function to format statistics values // -bool: Include a No Response/Unknown value datum // -This is only needed if at least 1 user did not respond to the X axis attribute on their profile. // -statisticsDatum.StatisticsDatum: The unknown value datum. // -error getStatisticsDatumsList := func()(int, []statisticsDatum.StatisticsDatum, map[string]int, bool, map[string]float64, func(float64)(string, error), bool, statisticsDatum.StatisticsDatum, error){ //TODO: Add "Probability Of ..." // This will allow viewing of choice attribute probabilities // For example: Probability of being Male, probability of being Female, etc. // We need to add this for each canonical option if (yAxisAttribute == "Number Of Users"){ totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, numberOfUnknownValueUsers, err := getProfileAttributeCountStatisticsDatumsList(identityType, networkType, xAxisAttribute, formatXAxisValuesFunction) if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } formatValuesFunction := func(input float64)(string, error){ // input will be an whole number representing the number of users result := helpers.ConvertFloat64ToStringRounded(input, 0) return result, nil } if (numberOfUnknownValueUsers == 0){ return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, false, nil, formatValuesFunction, false, statisticsDatum.StatisticsDatum{}, nil } unknownValueFormatted := helpers.ConvertIntToString(numberOfUnknownValueUsers) unknownDatum := statisticsDatum.StatisticsDatum{ Label: xAxisUnknownLabel, LabelFormatted: xAxisUnknownLabel, Value: float64(numberOfUnknownValueUsers), ValueFormatted: unknownValueFormatted, } return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, false, nil, formatValuesFunction, true, unknownDatum, nil } userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType) if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } yAxisIsAverage := strings.HasPrefix(yAxisAttribute, "Average ") if (yAxisIsAverage == true){ // This function will return the attribute name // We must convert the attribute title to the attribute name getAttributeToGetAverageFor := func()string{ attributeTitle := strings.TrimPrefix(yAxisAttribute, "Average ") switch attributeTitle{ case "Wealth":{ return "WealthInGold" } case "23andMe Neanderthal Variants":{ return "23andMe_NeanderthalVariants" } case "Body Fat":{ return "BodyFat" } case "Body Muscle":{ return "BodyMuscle" } case "Fruit Rating":{ return "FruitRating" } case "Vegetables Rating":{ return "VegetablesRating" } case "Nuts Rating":{ return "NutsRating" } case "Grains Rating":{ return "GrainsRating" } case "Dairy Rating":{ return "DairyRating" } case "Seafood Rating":{ return "SeafoodRating" } case "Beef Rating":{ return "BeefRating" } case "Pork Rating":{ return "PorkRating" } case "Poultry Rating":{ return "PoultryRating" } case "Eggs Rating":{ return "EggsRating" } case "Beans Rating":{ return "BeansRating" } case "Alcohol Frequency":{ return "AlcoholFrequency" } case "Tobacco Frequency":{ return "TobaccoFrequency" } case "Cannabis Frequency":{ return "CannabisFrequency" } case "Pets Rating":{ return "PetsRating" } case "Dogs Rating":{ return "DogsRating" } case "Cats Rating":{ return "CatsRating" } } return attributeTitle } attributeToGetAverageFor := getAttributeToGetAverageFor() totalAnalyzedUsers := 0 //Map structure: X-Axis attribute -> Number of users with this x-axis attribute who have a y-axis attribute response responseCountsMap := make(map[string]int) //Map structure: X-axis attribute response -> All Y-axis attribute responses summed for users with this x-axis attribute responseSumsMap := make(map[string]float64) // This stores a count of the number of users whose X axis value is unknown // For example, if X axis is Age, this is the number of users who did not respond numberOfUsersWithUnknownXAxisValue := 0 // This stores the total sum of all y axis values for users who have an unknown X axis value // For example, if the X axis attribute is age, and the Y axis attribute is average height, // this stores the sum of heights of all users who have no age on their profile. usersWithUnknownXAxisValueYAxisValuesSum := float64(0) for _, userIdentityHash := range userIdentityHashesToAnalyzeList{ profileFound, getAnyUserAttributeValueFunction, err := getRetrieveAnyAttributeFromUserNewestProfileFunction(userIdentityHash, networkType) if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } if (profileFound == false){ continue } userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled") if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } if (userIsDisabled == true){ continue } attributeExists, _, userAttributeToGetAverageForValue, err := getAnyUserAttributeValueFunction(attributeToGetAverageFor) if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } if (attributeExists == false){ // This user did not respond to the attribute we are getting the average for // We will not add them to the statistics maps continue } totalAnalyzedUsers += 1 userAttributeToGetAverageForValueFloat64, err := helpers.ConvertStringToFloat64(userAttributeToGetAverageForValue) if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, errors.New("Database corrupt: Contains invalid " + userAttributeToGetAverageForValue + " value: " + userAttributeToGetAverageForValue) } attributeFound, _, userXAxisAttributeValue, err := getAnyUserAttributeValueFunction(xAxisAttribute) if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } if (attributeFound == false){ // This user did not respond to the X axis attribute // We calculate the average for users who do not respond and put it in its own category numberOfUsersWithUnknownXAxisValue += 1 usersWithUnknownXAxisValueYAxisValuesSum += userAttributeToGetAverageForValueFloat64 continue } responseCountsMap[userXAxisAttributeValue] += 1 responseSumsMap[userXAxisAttributeValue] += userAttributeToGetAverageForValueFloat64 } _, _, formatYAxisValuesFunction, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeToGetAverageFor) if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } statisticsDatumsList := make([]statisticsDatum.StatisticsDatum, 0, len(responseCountsMap)) for attributeResponse, responsesCount := range responseCountsMap{ attributeResponseFormatted, err := formatXAxisValuesFunction(attributeResponse) if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } allResponsesSum, exists := responseSumsMap[attributeResponse] if (exists == false){ return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, errors.New("Response sums map missing attribute value") } averageValue := allResponsesSum/float64(responsesCount) averageValueString := helpers.ConvertFloat64ToStringRounded(averageValue, yAxisRoundingPrecision) averageValueFormatted, err := formatYAxisValuesFunction(averageValueString) if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } newStatisticsDatum := statisticsDatum.StatisticsDatum{ Label: attributeResponse, LabelFormatted: attributeResponseFormatted, Value: averageValue, ValueFormatted: averageValueFormatted, } statisticsDatumsList = append(statisticsDatumsList, newStatisticsDatum) } // We use this function to format values after grouping, if grouping is needed formatValuesFunction := func(input float64)(string, error){ inputString := helpers.ConvertFloat64ToStringRounded(input, yAxisRoundingPrecision) valueFormatted, err := formatYAxisValuesFunction(inputString) if (err != nil) { return "", err } return valueFormatted, nil } if (numberOfUsersWithUnknownXAxisValue == 0){ return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, false, statisticsDatum.StatisticsDatum{}, nil } unknownResponsesAverage := usersWithUnknownXAxisValueYAxisValuesSum/float64(numberOfUsersWithUnknownXAxisValue) unknownResponsesValueFormatted, err := formatValuesFunction(unknownResponsesAverage) if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } // This datum represents the average value for the yAxisAttribute for users who did not respond. // For example, if the xAxisAttribute is Height, and the yAxisAttribute is AverageWealth, this value // will represent the average wealth for users who did not provide Height on their profile. unknownStatisticsDatum := statisticsDatum.StatisticsDatum{ Label: xAxisUnknownLabel, LabelFormatted: xAxisUnknownLabel, Value: unknownResponsesAverage, ValueFormatted: unknownResponsesValueFormatted, } return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, true, unknownStatisticsDatum, nil } return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, errors.New("Invalid y-axis attribute: " + yAxisAttribute) } totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction, includeUnknownDatum, unknownValueDatum, err := getStatisticsDatumsList() if (err != nil) { return 0, nil, false, nil, nil, err } sortStatisticsDatumsList(statisticsDatumsList, xAxisIsNumerical) // We now see if we need to group the datums in the list together // We do this if there are more than 10 categories if (len(statisticsDatumsList) <= 10){ // No grouping needed. We are done. if (includeUnknownDatum == true){ statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum) } return totalAnalyzedUsers, statisticsDatumsList, false, nil, formatValuesFunction, nil } groupedStatisticsDatumsList, err := getStatisticsDatumsListGrouped(10, statisticsDatumsList, xAxisIsNumerical, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction) if (err != nil) { return 0, nil, false, nil, nil, err } if (includeUnknownDatum == true){ statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum) groupedStatisticsDatumsList = append(groupedStatisticsDatumsList, unknownValueDatum) } return totalAnalyzedUsers, statisticsDatumsList, true, groupedStatisticsDatumsList, formatValuesFunction, nil } //Outputs: // -int: Number of users analyzed in statistics // -[]statisticsDatum.StatisticsDatum: Statistics datums list (sorted, not grouped) // -bool: Grouping performed // -[]statisticsDatum.StatisticsDatum: Grouped datums list // -error func GetUserStatisticsDatumsLists_DonutChart(identityType string, networkType byte, attributeToAnalyze string, attributeIsNumerical bool, formatAttributeLabelsFunction func(string)(string, error), unknownLabelTranslated string)(int, []statisticsDatum.StatisticsDatum, bool, []statisticsDatum.StatisticsDatum, error){ isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return 0, nil, false, nil, errors.New("GetUserStatisticsDatumsLists_DonutChart called with invalid networkType: " + networkTypeString) } totalAnalyzedUsers, statisticsDatumsList, attributeCountsMap, numberOfUnknownResponders, err := getProfileAttributeCountStatisticsDatumsList(identityType, networkType, attributeToAnalyze, formatAttributeLabelsFunction) if (err != nil) { return 0, nil, false, nil, err } sortStatisticsDatumsList(statisticsDatumsList, attributeIsNumerical) getUnknownValueDatum := func()statisticsDatum.StatisticsDatum{ numberOfUnknownRespondersString := helpers.ConvertIntToString(numberOfUnknownResponders) unknownValueDatum := statisticsDatum.StatisticsDatum{ Label: unknownLabelTranslated, LabelFormatted: unknownLabelTranslated, Value: float64(numberOfUnknownResponders), ValueFormatted: numberOfUnknownRespondersString, } return unknownValueDatum } if (len(statisticsDatumsList) <= 8){ // No grouping needed. if (numberOfUnknownResponders != 0){ unknownValueDatum := getUnknownValueDatum() statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum) } return totalAnalyzedUsers, statisticsDatumsList, false, nil, nil } formatValuesFunction := func(input float64)(string, error){ // input will always be the number of users who responded with the value // Thus, input will always be an integer result := helpers.ConvertFloat64ToStringRounded(input, 0) return result, nil } groupedStatisticsDatumsList, err := getStatisticsDatumsListGrouped(8, statisticsDatumsList, attributeIsNumerical, attributeCountsMap, false, nil, formatValuesFunction) if (err != nil) { return 0, nil, false, nil, err } if (numberOfUnknownResponders != 0){ unknownValueDatum := getUnknownValueDatum() statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum) groupedStatisticsDatumsList = append(groupedStatisticsDatumsList, unknownValueDatum) } return totalAnalyzedUsers, statisticsDatumsList, true, groupedStatisticsDatumsList, nil } // This function will return a statistics datums list of the following format: // "Label": Attribute name (Example: "Male") // "Value": The number of users who responded with the attribute (in this example: "Male") // // All users of provided identityType who are not disabled will be analyzed // -int: Number of analyzed users // -[]statisticsDatum.StatisticsDatum: Statistics datums list (not sorted or grouped) // -map[string]int: Response counts map (Response -> Number of responders) // -int: Number of No Response/Unknown value responders // -error func getProfileAttributeCountStatisticsDatumsList(identityType string, networkType byte, attributeName string, formatLabelsFunction func(string)(string, error))(int, []statisticsDatum.StatisticsDatum, map[string]int, int, error){ userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType) if (err != nil) { return 0, nil, nil, 0, err } totalAnalyzedUsers := 0 // Map structure: User Attribute response -> Number of users who responded with the response responseCountsMap := make(map[string]int) // This stores the number of users for whom we do not know their value numberOfUnknownValueUsers := 0 for _, userIdentityHash := range userIdentityHashesToAnalyzeList{ profileFound, getAnyUserAttributeValueFunction, err := getRetrieveAnyAttributeFromUserNewestProfileFunction(userIdentityHash, networkType) if (err != nil) { return 0, nil, nil, 0, err } if (profileFound == false){ continue } userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled") if (err != nil) { return 0, nil, nil, 0, err } if (userIsDisabled == true){ continue } totalAnalyzedUsers += 1 attributeFound, _, attributeValue, err := getAnyUserAttributeValueFunction(attributeName) if (err != nil) { return 0, nil, nil, 0, err } if (attributeFound == false){ numberOfUnknownValueUsers += 1 continue } responseCountsMap[attributeValue] += 1 } statisticsDatumsList := make([]statisticsDatum.StatisticsDatum, 0, len(responseCountsMap)) for attributeResponse, numberOfUsers := range responseCountsMap{ attributeResponseFormatted, err := formatLabelsFunction(attributeResponse) if (err != nil) { return 0, nil, nil, 0, err } attributeNumberOfUsersString := helpers.ConvertIntToString(numberOfUsers) newStatisticsDatum := statisticsDatum.StatisticsDatum{ Label: attributeResponse, LabelFormatted: attributeResponseFormatted, Value: float64(numberOfUsers), ValueFormatted: attributeNumberOfUsersString, } statisticsDatumsList = append(statisticsDatumsList, newStatisticsDatum) } return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, numberOfUnknownValueUsers, nil } func sortStatisticsDatumsList(inputStatisticsDatumsList []statisticsDatum.StatisticsDatum, labelIsNumerical bool){ if (len(inputStatisticsDatumsList) <= 1){ return } if (labelIsNumerical == true){ // We sort the datums by label values in ascending order // Example: Bar chart columns are ages, in order of youngest to oldest compareDatumsFunction := func(datumA statisticsDatum.StatisticsDatum, datumB statisticsDatum.StatisticsDatum)int{ datumALabel := datumA.Label datumBLabel := datumB.Label datumAFloat64, err := helpers.ConvertStringToFloat64(datumALabel) if (err != nil) { panic("Invalid statistics datum: Datum Label is not float: " + datumALabel) } datumBFloat64, err := helpers.ConvertStringToFloat64(datumBLabel) if (err != nil) { panic("Invalid statistics datum: Datum Label is not float: " + datumBLabel) } if (datumAFloat64 == datumBFloat64){ return 0 } if (datumAFloat64 < datumBFloat64){ return -1 } return 1 } slices.SortFunc(inputStatisticsDatumsList, compareDatumsFunction) return } // We sort the datums by their values in descending order compareDatumsFunction := func(datum1 statisticsDatum.StatisticsDatum, datum2 statisticsDatum.StatisticsDatum)int{ datum1Value := datum1.Value datum2Value := datum2.Value if (datum1Value == datum2Value){ return 0 } if (datum1Value < datum2Value){ return 1 } return -1 } slices.SortFunc(inputStatisticsDatumsList, compareDatumsFunction) } // This function will group a statistics datums list. // It will group Labels and their values to fit into a specified number of groups // Example: "1","2","3","4" -> "1-2", "3-4" //Inputs: // -int: Maximum groups to create // -[]statisticsDatum.StatisticsDatum: Statistics datums list to group // -bool: Label is numerical // -If it is, we will group labels into groups of numbers. // -Otherwise, we will group all categories after the first maximumGroupsToCreate into a group called Other // -map[string]int // -Response Counts map (Attribute response -> Number of responders with that response) // -bool: Value is average // -If value is average, we will combine the values of each group and find their average // -Otherwise, we will find their sum // -map[string]float64 // -Response Sums map (X-axis attribute response -> all Y-axis responses summed) (If yAxisIsAverage == true) // -func(float64)(string, error): This is the function we use to format the values //Outputs: // -[]statisticsDatum.StatisticsDatum: Grouped statistics datums list // -error func getStatisticsDatumsListGrouped(maximumGroupsToCreate int, inputStatisticsDatumsList []statisticsDatum.StatisticsDatum, labelIsNumerical bool, responseCountsMap map[string]int, valueIsAverage bool, responseSumsMap map[string]float64, formatValuesFunction func(float64)(string, error))([]statisticsDatum.StatisticsDatum, error){ if (len(inputStatisticsDatumsList) <= maximumGroupsToCreate){ return nil, errors.New("maximumGroupsToCreate is <= length of input statistics datums list") } // We deep copy the statistics datums list to retain the sorted version // We need to retain both versions because the user can view the raw or grouped data in the GUI statisticsDatumsList := slices.Clone(inputStatisticsDatumsList) // We use this function to get the new value for a group of labels getGroupValue := func(datumsToCombineList []statisticsDatum.StatisticsDatum)(float64, error){ // This will count the total number of users who responded with the responses within this group // Example: Labels are "Blue", "Green", this variable will store the number of users who responded with either Blue or Green totalRespondersCount := float64(0) // This will store the sum for all response values within the group // We only need to add to this sum if valueIsAverage == true allReponsesSummed := float64(0) for _, statisticsDatum := range datumsToCombineList{ datumLabel := statisticsDatum.Label responderCount, exists := responseCountsMap[datumLabel] if (exists == false){ return 0, errors.New("responseCountsMap missing label: " + datumLabel) } totalRespondersCount += float64(responderCount) if (valueIsAverage == true){ yAxisAttributeResponsesSum, exists := responseSumsMap[datumLabel] if (exists == false){ return 0, errors.New("responseSumsMap missing label: " + datumLabel) } allReponsesSummed += yAxisAttributeResponsesSum } } if (valueIsAverage == false){ return totalRespondersCount, nil } // The value is an average // We need to find the average for all of the user responses for the labels in the input list // The Values in the inputStatisticsDatumsList are averages // We can't average out the averages, because that will not give us the true average // We have to use the original sums for all group datums and average them if (totalRespondersCount == 0){ return 0, errors.New("totalRespondersCount is 0.") } value := allReponsesSummed/float64(totalRespondersCount) return value, nil } if (labelIsNumerical == true){ maximumDatumsPerCategory := int(math.Ceil(float64(len(statisticsDatumsList))/float64(maximumGroupsToCreate))) statisticsDatumsListSublists, err := helpers.SplitListIntoSublists(statisticsDatumsList, maximumDatumsPerCategory) if (err != nil) { return nil, err } groupedDatumsList := make([]statisticsDatum.StatisticsDatum, 0, len(statisticsDatumsListSublists)) for _, groupDatumsListSublist := range statisticsDatumsListSublists{ if (len(groupDatumsListSublist) == 1){ // Sometimes, a group with 1 datum will be created // This happens if the groups cannot be evenly divided, so there is a remainder of 1. // Example: 10->4 groups = 3, 3, 3, 1. //TODO: Prevent this from happening so groups always have more than 1 subdatum groupDatum := groupDatumsListSublist[0] groupedDatumsList = append(groupedDatumsList, groupDatum) continue } finalIndex := len(groupDatumsListSublist)-1 initialDatum := groupDatumsListSublist[0] finalDatum := groupDatumsListSublist[finalIndex] initialLabel := initialDatum.Label initialLabelFormatted := initialDatum.LabelFormatted finalLabel := finalDatum.Label finalLabelFormatted := finalDatum.LabelFormatted groupValue, err := getGroupValue(groupDatumsListSublist) if (err != nil) { return nil, err } groupValueFormatted, err := formatValuesFunction(groupValue) if (err != nil) { return nil, err } newGroupStatisticsDatum := statisticsDatum.StatisticsDatum{ Label: initialLabel + "-" + finalLabel, LabelFormatted: initialLabelFormatted + "-" + finalLabelFormatted, Value: groupValue, ValueFormatted: groupValueFormatted, } groupedDatumsList = append(groupedDatumsList, newGroupStatisticsDatum) } return groupedDatumsList, nil } // Label is not numerical // We combine all categories after the first maximumGroupsToCreate into a category called Other datumsToKeep := statisticsDatumsList[:maximumGroupsToCreate] datumsToCombine := statisticsDatumsList[maximumGroupsToCreate:] otherTranslated := translation.TranslateTextFromEnglishToMyLanguage("Other") otherGroupValue, err := getGroupValue(datumsToCombine) if (err != nil) { return nil, err } otherGroupValueFormatted, err := formatValuesFunction(otherGroupValue) if (err != nil) { return nil, err } otherGroupDatum := statisticsDatum.StatisticsDatum{ Label: "Other", LabelFormatted: otherTranslated, Value: otherGroupValue, ValueFormatted: otherGroupValueFormatted, } groupedStatisticsDatumsList := append(datumsToKeep, otherGroupDatum) return groupedStatisticsDatumsList, nil } func getUserIdentityHashesToAnalyzeList(identityType string)([][16]byte, error){ allUserIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes(identityType) if (err != nil) { return nil, err } if (len(allUserIdentityHashesList) < 10000){ return allUserIdentityHashesList, nil } helpers.RandomizeListOrder(allUserIdentityHashesList) identityHashesToAnalyzeList := allUserIdentityHashesList[:10000] return identityHashesToAnalyzeList, nil } //Outputs: // -bool: Profile Exists // -func(string)(bool, int, string, error) // -error func getRetrieveAnyAttributeFromUserNewestProfileFunction(identityHash [16]byte, networkType byte)(bool, func(string)(bool, int, string, error), error){ newestProfileExists, profileVersion, _, _, _, newestProfileMap, err := profileStorage.GetNewestUserProfile(identityHash, networkType) if (err != nil) { return false, nil, err } if (newestProfileExists == false){ return false, nil, nil } getAnyAttributeFromUserNewestProfileFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, newestProfileMap) if (err != nil) { return false, nil, err } return true, getAnyAttributeFromUserNewestProfileFunction, nil }