// 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 "slices" import "strings" import "errors" import "math" type StatisticsItem struct{ // The label for the statistics item // For a bar chart, this represents the name of an X axis bar. // For a donut chart, this represents the name of a donut slice // Example: "Man", "100-200" // This will never be translated, unless it is the Unknown/No Response item, in which case // the label will be "Unknown"/"No Response" translated to the user's current app language Label string // This is the formatted, human readable version of the label // This will be translated into the application language // Sometimes, the LabelFormatted will be identical to Label // This does not include units (Example: " days", " users") // Example: // -"1000000-2000000" -> "1 million-2 million" LabelFormatted string // The value corresponding to the label // For a bar chart, this represents the Y axis value for a bar. // For a donut chart, this represents the value (size) of one of the donut slices // This will never be translated // For example, the value could be 500 if 500 men responded Yes. Value float64 // This is the formatted version of the value // This does not include units (Example: " days", " users") // Examples: // -5 -> "5/10" // -1500000000000 -> "1.5 trillion" ValueFormatted string } //Outputs: // -int: Number of users analyzed in statistics // -[]StatisticsItem: Statistics items list (sorted, not grouped) // -bool: Grouping performed // -[]StatisticsItem: Grouped items 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 GetUserStatisticsItemsLists_BarChart(identityType string, networkType byte, xAxisAttribute string, xAxisIsNumerical bool, formatXAxisValuesFunction func(string)(string, error), xAxisUnknownLabel string, yAxisAttribute string)(int, []StatisticsItem, bool, []StatisticsItem, func(float64)(string, error), error){ isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return 0, nil, false, nil, nil, errors.New("GetUserStatisticsItemsLists_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 // -[]StatisticsItem: Statistics items 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 item // -This is only needed if at least 1 user did not respond to the X axis attribute on their profile. // -StatisticsItem: The unknown value item. // -error getStatisticsItemsList := func()(int, []StatisticsItem, map[string]int, bool, map[string]float64, func(float64)(string, error), bool, StatisticsItem, 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, statisticsItemsList, responseCountsMap, numberOfUnknownValueUsers, err := getProfileAttributeCountStatisticsItemsList(identityType, networkType, xAxisAttribute, formatXAxisValuesFunction) if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, 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, statisticsItemsList, responseCountsMap, false, nil, formatValuesFunction, false, StatisticsItem{}, nil } unknownValueFormatted := helpers.ConvertIntToString(numberOfUnknownValueUsers) unknownItem := StatisticsItem{ Label: xAxisUnknownLabel, LabelFormatted: xAxisUnknownLabel, Value: float64(numberOfUnknownValueUsers), ValueFormatted: unknownValueFormatted, } return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, false, nil, formatValuesFunction, true, unknownItem, nil } userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType) if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, 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 ") if (attributeTitle == "Wealth"){ return "WealthInGold" } if (attributeTitle == "23andMe Neanderthal Variants"){ return "23andMe_NeanderthalVariants" } if (attributeTitle == "Body Fat"){ return "BodyFat" } if (attributeTitle == "Body Muscle"){ return "BodyMuscle" } if (attributeTitle == "Fruit Rating"){ return "FruitRating" } if (attributeTitle == "Vegetables Rating"){ return "VegetablesRating" } if (attributeTitle == "Nuts Rating"){ return "NutsRating" } if (attributeTitle == "Grains Rating"){ return "GrainsRating" } if (attributeTitle == "Dairy Rating"){ return "DairyRating" } if (attributeTitle == "Seafood Rating"){ return "SeafoodRating" } if (attributeTitle == "Beef Rating"){ return "BeefRating" } if (attributeTitle == "Pork Rating"){ return "PorkRating" } if (attributeTitle == "Poultry Rating"){ return "PoultryRating" } if (attributeTitle == "Eggs Rating"){ return "EggsRating" } if (attributeTitle == "Beans Rating"){ return "BeansRating" } if (attributeTitle == "Alcohol Frequency"){ return "AlcoholFrequency" } if (attributeTitle == "Tobacco Frequency"){ return "TobaccoFrequency" } if (attributeTitle == "Cannabis Frequency"){ return "CannabisFrequency" } if (attributeTitle == "Pets Rating"){ return "PetsRating" } if (attributeTitle == "Dogs Rating"){ return "DogsRating" } if (attributeTitle == "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, StatisticsItem{}, err } if (profileFound == false){ continue } userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled") if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } if (userIsDisabled == true){ continue } attributeExists, _, userAttributeToGetAverageForValue, err := getAnyUserAttributeValueFunction(attributeToGetAverageFor) if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, 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, StatisticsItem{}, 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, StatisticsItem{}, 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, StatisticsItem{}, err } statisticsItemsList := make([]StatisticsItem, 0, len(responseCountsMap)) for attributeResponse, responsesCount := range responseCountsMap{ attributeResponseFormatted, err := formatXAxisValuesFunction(attributeResponse) if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } allResponsesSum, exists := responseSumsMap[attributeResponse] if (exists == false){ return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, 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, StatisticsItem{}, err } newStatisticsItem := StatisticsItem{ Label: attributeResponse, LabelFormatted: attributeResponseFormatted, Value: averageValue, ValueFormatted: averageValueFormatted, } statisticsItemsList = append(statisticsItemsList, newStatisticsItem) } // 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, statisticsItemsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, false, StatisticsItem{}, nil } unknownResponsesAverage := usersWithUnknownXAxisValueYAxisValuesSum/float64(numberOfUsersWithUnknownXAxisValue) unknownResponsesValueFormatted, err := formatValuesFunction(unknownResponsesAverage) if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } // This item 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. unknownStatisticsItem := StatisticsItem{ Label: xAxisUnknownLabel, LabelFormatted: xAxisUnknownLabel, Value: unknownResponsesAverage, ValueFormatted: unknownResponsesValueFormatted, } return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, true, unknownStatisticsItem, nil } return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, errors.New("Invalid y-axis attribute: " + yAxisAttribute) } totalAnalyzedUsers, statisticsItemsList, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction, includeUnknownItem, unknownValueItem, err := getStatisticsItemsList() if (err != nil) { return 0, nil, false, nil, nil, err } sortStatisticsItemsList(statisticsItemsList, xAxisIsNumerical) // We now see if we need to group the items in the list together // We do this if there are more than 10 categories if (len(statisticsItemsList) <= 10){ // No grouping needed. We are done. if (includeUnknownItem == true){ statisticsItemsList = append(statisticsItemsList, unknownValueItem) } return totalAnalyzedUsers, statisticsItemsList, false, nil, formatValuesFunction, nil } groupedStatisticsItemsList, err := getStatisticsItemsListGrouped(10, statisticsItemsList, xAxisIsNumerical, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction) if (err != nil) { return 0, nil, false, nil, nil, err } if (includeUnknownItem == true){ statisticsItemsList = append(statisticsItemsList, unknownValueItem) groupedStatisticsItemsList = append(groupedStatisticsItemsList, unknownValueItem) } return totalAnalyzedUsers, statisticsItemsList, true, groupedStatisticsItemsList, formatValuesFunction, nil } //Outputs: // -int: Number of users analyzed in statistics // -[]StatisticsItem: Statistics items list (sorted, not grouped) // -bool: Grouping performed // -[]StatisticsItem: Grouped items list // -error func GetUserStatisticsItemsLists_DonutChart(identityType string, networkType byte, attributeToAnalyze string, attributeIsNumerical bool, formatAttributeLabelsFunction func(string)(string, error), unknownLabelTranslated string)(int, []StatisticsItem, bool, []StatisticsItem, error){ isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) return 0, nil, false, nil, errors.New("GetUserStatisticsItemsLists_DonutChart called with invalid networkType: " + networkTypeString) } totalAnalyzedUsers, statisticsItemsList, attributeCountsMap, numberOfUnknownResponders, err := getProfileAttributeCountStatisticsItemsList(identityType, networkType, attributeToAnalyze, formatAttributeLabelsFunction) if (err != nil) { return 0, nil, false, nil, err } sortStatisticsItemsList(statisticsItemsList, attributeIsNumerical) getUnknownValueItem := func()StatisticsItem{ numberOfUnknownRespondersString := helpers.ConvertIntToString(numberOfUnknownResponders) unknownValueItem := StatisticsItem{ Label: unknownLabelTranslated, LabelFormatted: unknownLabelTranslated, Value: float64(numberOfUnknownResponders), ValueFormatted: numberOfUnknownRespondersString, } return unknownValueItem } if (len(statisticsItemsList) <= 8){ // No grouping needed. if (numberOfUnknownResponders != 0){ unknownValueItem := getUnknownValueItem() statisticsItemsList = append(statisticsItemsList, unknownValueItem) } return totalAnalyzedUsers, statisticsItemsList, 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 } groupedStatisticsItemsList, err := getStatisticsItemsListGrouped(8, statisticsItemsList, attributeIsNumerical, attributeCountsMap, false, nil, formatValuesFunction) if (err != nil) { return 0, nil, false, nil, err } if (numberOfUnknownResponders != 0){ unknownValueItem := getUnknownValueItem() statisticsItemsList = append(statisticsItemsList, unknownValueItem) groupedStatisticsItemsList = append(groupedStatisticsItemsList, unknownValueItem) } return totalAnalyzedUsers, statisticsItemsList, true, groupedStatisticsItemsList, nil } // This function will return a statistics items 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 // -[]StatisticsItem: Statistics items 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 getProfileAttributeCountStatisticsItemsList(identityType string, networkType byte, attributeName string, formatLabelsFunction func(string)(string, error))(int, []StatisticsItem, 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 } statisticsItemsList := make([]StatisticsItem, 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) newStatisticsItem := StatisticsItem{ Label: attributeResponse, LabelFormatted: attributeResponseFormatted, Value: float64(numberOfUsers), ValueFormatted: attributeNumberOfUsersString, } statisticsItemsList = append(statisticsItemsList, newStatisticsItem) } return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, numberOfUnknownValueUsers, nil } func sortStatisticsItemsList(inputStatisticsItemsList []StatisticsItem, labelIsNumerical bool){ if (len(inputStatisticsItemsList) <= 1){ return } if (labelIsNumerical == true){ // We sort the items by label values in ascending order // Example: Bar chart columns are ages, in order of youngest to oldest compareItemsFunction := func(itemA StatisticsItem, itemB StatisticsItem)int{ itemALabel := itemA.Label itemBLabel := itemB.Label itemAFloat64, err := helpers.ConvertStringToFloat64(itemALabel) if (err != nil) { panic("Invalid statistics item: Item Label is not float: " + itemALabel) } itemBFloat64, err := helpers.ConvertStringToFloat64(itemBLabel) if (err != nil) { panic("Invalid statistics item: Item Label is not float: " + itemBLabel) } if (itemAFloat64 == itemBFloat64){ return 0 } if (itemAFloat64 < itemBFloat64){ return -1 } return 1 } slices.SortFunc(inputStatisticsItemsList, compareItemsFunction) return } // We sort the items by their values in descending order compareItemsFunction := func(itemA StatisticsItem, itemB StatisticsItem)int{ itemAValue := itemA.Value itemBValue := itemB.Value if (itemAValue == itemBValue){ return 0 } if (itemAValue < itemBValue){ return 1 } return -1 } slices.SortFunc(inputStatisticsItemsList, compareItemsFunction) } // This function will group a statistics items 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 // -[]StatisticsItem: Statistics items 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: // -[]StatisticsItem: Grouped statistics items list // -error func getStatisticsItemsListGrouped(maximumGroupsToCreate int, inputStatisticsItemsList []StatisticsItem, labelIsNumerical bool, responseCountsMap map[string]int, valueIsAverage bool, responseSumsMap map[string]float64, formatValuesFunction func(float64)(string, error))([]StatisticsItem, error){ if (len(inputStatisticsItemsList) <= maximumGroupsToCreate){ return nil, errors.New("maximumGroupsToCreate is <= length of input statistics items list") } // We deep copy the statistics items 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 statisticsItemsList := slices.Clone(inputStatisticsItemsList) // We use this function to get the new value for a group of labels getGroupValue := func(itemsToCombineList []StatisticsItem)(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 _, statisticsItem := range itemsToCombineList{ itemLabel := statisticsItem.Label responderCount, exists := responseCountsMap[itemLabel] if (exists == false){ return 0, errors.New("responseCountsMap missing label: " + itemLabel) } totalRespondersCount += float64(responderCount) if (valueIsAverage == true){ yAxisAttributeResponsesSum, exists := responseSumsMap[itemLabel] if (exists == false){ return 0, errors.New("responseSumsMap missing label: " + itemLabel) } 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 inputStatisticsItemsList 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 items and average them if (totalRespondersCount == 0){ return 0, errors.New("totalRespondersCount is 0.") } value := allReponsesSummed/float64(totalRespondersCount) return value, nil } if (labelIsNumerical == true){ maximumItemsPerCategory := int(math.Ceil(float64(len(statisticsItemsList))/float64(maximumGroupsToCreate))) statisticsItemsListSublists, err := helpers.SplitListIntoSublists(statisticsItemsList, maximumItemsPerCategory) if (err != nil) { return nil, err } groupedItemsList := make([]StatisticsItem, 0, len(statisticsItemsListSublists)) for _, groupItemsListSublist := range statisticsItemsListSublists{ if (len(groupItemsListSublist) == 1){ // Sometimes, a group with 1 item 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 subitem groupItem := groupItemsListSublist[0] groupedItemsList = append(groupedItemsList, groupItem) continue } finalIndex := len(groupItemsListSublist)-1 initialItem := groupItemsListSublist[0] finalItem := groupItemsListSublist[finalIndex] initialLabel := initialItem.Label initialLabelFormatted := initialItem.LabelFormatted finalLabel := finalItem.Label finalLabelFormatted := finalItem.LabelFormatted groupValue, err := getGroupValue(groupItemsListSublist) if (err != nil) { return nil, err } groupValueFormatted, err := formatValuesFunction(groupValue) if (err != nil) { return nil, err } newGroupStatisticsItem := StatisticsItem{ Label: initialLabel + "-" + finalLabel, LabelFormatted: initialLabelFormatted + "-" + finalLabelFormatted, Value: groupValue, ValueFormatted: groupValueFormatted, } groupedItemsList = append(groupedItemsList, newGroupStatisticsItem) } return groupedItemsList, nil } // Label is not numerical // We combine all categories after the first maximumGroupsToCreate into a category called Other itemsToKeep := statisticsItemsList[:maximumGroupsToCreate] itemsToCombine := statisticsItemsList[maximumGroupsToCreate:] otherTranslated := translation.TranslateTextFromEnglishToMyLanguage("Other") otherGroupValue, err := getGroupValue(itemsToCombine) if (err != nil) { return nil, err } otherGroupValueFormatted, err := formatValuesFunction(otherGroupValue) if (err != nil) { return nil, err } otherGroupItem := StatisticsItem{ Label: "Other", LabelFormatted: otherTranslated, Value: otherGroupValue, ValueFormatted: otherGroupValueFormatted, } groupedStatisticsItemsList := append(itemsToKeep, otherGroupItem) return groupedStatisticsItemsList, 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 }