801 lines
28 KiB
Go
801 lines
28 KiB
Go
|
|
||
|
// 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
|
||
|
}
|
||
|
|