예제 #1
0
def new_report(os=OS.ios,
               pattern="ВНИМАНИЕ! ЗАДАЙТЕ ПАТТЕРНЫ ВРУЧНУЮ.",
               days_since_install=28,
               min_app_version="4.7",
               min_install_version="6.5",
               min_quest=None,
               exact=False,
               classic_retention=True):
    """
    Расчет накопительного ARPU и ROI по паблишерам и трекинговым ссылкам(источникам)
    :param period_start: начало периода
    :param period_end: конец периода
    :param days_since_install: рассчитное кол-во дней после установки
    :param min_app_version: мин версия приложения
    :param exact: точное соответствие версии приложения
    :return:
    """

    # БАЗА ДАННЫХ
    sql = """
        SELECT ios_ifa, ios_ifv, event_name, event_json, event_datetime, app_version_name
        FROM sop_events.events_ios
        where 
        (
        event_name = "CityEvent" and (      event_json like '{"Quest%'
                                            or
                                            event_json like "%BuyDecoration%"
                                            or
                                            event_json like "%InitGameState%"
                                            or
                                            event_json like "%BuyHealth%"
                                            or
                                            event_json like "%BuyPremiumCoin%Success%"
                                        )
        or event_name = "Match3Events" and (
                                                event_json like "%BuyPremiumCoinM3%Success%"
                                                or
                                                event_json like "%CompleteTargets%"
                                                or
                                                event_json like "%FailGame%"
                                            )
                                                
        )                      
        and
        (ios_ifv<>"" or ios_ifa <>"")
        order by ios_ifa, ios_ifv,  event_datetime
        """
    report = Report(sql_events=sql,
                    min_app_version=min_app_version,
                    exact=exact,
                    get_installs=True,
                    min_install_version=min_install_version)

    # ПАТТЕРНЫ
    # ВАЖНО! МНОЖЕСТВА ПОЛЬЗОВАТЕЛЕЙ, ПОДХОДЯЩИХ ПОД ПАТТЕРНЫ НЕ ДОЛЖНЫ ПЕРЕСЕКАТЬСЯ! (есть вывод по пропущенным)
    '''
    patterns_list = [
        pattern_BuyDecoration(completed_once=False, min_completions=2),
        pattern_BuyDecoration(completed_once=True),
        pattern_BuyDecoration(completed_once=False, max_completions=0, same_day_pattern=False)
    ]
    ''' '''
    patterns_list = [
        pattern_BuyHealth(completed_once=False, min_completions=2),
        pattern_BuyHealth(completed_once=True),
        pattern_BuyHealth(completed_once=False, max_completions=0, same_day_pattern=False)
    ]
    '''
    patterns_list = [
        pattern_WinRow(completed_once=False,
                       min_completions=2,
                       same_session_pattern=True),
        pattern_WinRow(completed_once=True, same_session_pattern=True),
        pattern_WinRow(completed_once=False,
                       max_completions=0,
                       same_day_pattern=True,
                       same_session_pattern=False)
    ]
    # ПАРАМЕТРЫ
    parameters = ["Retention Period", "Pattern", "Users"]
    accumulating_parameters = [
        str(i) + "d" for i in range(0, days_since_install + 1)
    ]
    parameters += accumulating_parameters
    # ПЕРИОДЫ РЕТЕНШЕНА
    retention_periods = ["0", "1-2", "3-6", "7-13", "14-27", "28+", "Never"]
    pattern_analysis = {}
    for retention_period in retention_periods:
        pattern_analysis[retention_period] = {}
        for pattern_name in [pattern.name for pattern in patterns_list]:
            pattern_analysis[retention_period][pattern_name] = dict.fromkeys(
                ["Users"] + accumulating_parameters, 0)

    # функции ретеншена
    def get_retention_period(d):
        if d == 0:
            return "0"
        elif d < 3:
            return "1-2"
        elif d < 7:
            return "3-6"
        elif d < 14:
            return "7-13"
        elif d < 28:
            return "14-27"
        else:
            return "28+"

    def get_max_retention_day(user_entries_list,
                              classic_retention=classic_retention):
        if classic_retention:
            for i in range(1, len(user_entries_list)):
                if user_entries_list[i] == 0:
                    return i - 1
        else:
            for i in range(len(user_entries_list) - 1, -1, -1):
                if user_entries_list[i] > 0:
                    return i

    def make_rolling_retention(user_entries_list):
        for i in range(len(user_entries_list) - 1, 0, -1):
            if user_entries_list[i - 1] < user_entries_list[i]:
                user_entries_list[i - 1] = user_entries_list[i]
        return user_entries_list

    # Пользовательские параметры
    user_session_events = []
    user_day_events = []
    previous_day_in_game = 0
    if min_quest:
        min_quest = CityEventsQuest(quest_id=min_quest,
                                    action="Take",
                                    datetime=None)
    user_retention = [0] * (days_since_install + 1)
    user_events = {}
    for i in range(0, (days_since_install + 1)):
        user_events[i] = []

    # Запись пользовательских данных в общие
    def flush_user_data():
        user_found_pattern = False
        # проверяем каждый паттерн
        for pattern in patterns_list:
            # постепенно собираем данные в один массив для паттерном, которые рассматриваются на всех событиях в куче
            user_accumulating_events = []
            # выполнение паттерна на отрезке ретеншена
            user_pattern_completion = dict.fromkeys(retention_periods, 0)
            # по каждому дню лайфтайма
            for day in range(0, days_since_install + 1):
                user_day_accumulated_events = []
                for session_events in user_events[day]:
                    # паттерн по каждой сессии
                    if pattern.same_day_pattern and pattern.same_session_pattern:
                        user_pattern_completion[get_retention_period(
                            day)] += int(pattern.is_followed(session_events))
                    user_day_accumulated_events += session_events
                    user_accumulating_events += session_events
                # паттер по дыннм с одного дня
                if pattern.same_day_pattern and not pattern.same_session_pattern:
                    user_pattern_completion[get_retention_period(day)] += int(
                        pattern.is_followed(user_day_accumulated_events))
                # паттерн по всем данным всех дней
                if not pattern.same_day_pattern:
                    user_pattern_completion[get_retention_period(day)] += int(
                        pattern.is_followed(user_accumulating_events))

            # суммарное выполнение паттерна
            overall_pattern_completions = 0
            # окончательный период выполнения паттерна
            final_completion_period = None
            for period in retention_periods:
                # если паттерн на совершение события, то берем первый период его выполнения
                if not final_completion_period and user_pattern_completion[
                        period] > 0 and (pattern.max_completions is not None
                                         and pattern.max_completions > 0
                                         or pattern.max_completions is None):
                    final_completion_period = period
                overall_pattern_completions += user_pattern_completion[period]
            # если паттерн выполнен меньше минимума или больше максимума раз - выходим, не подходит
            if (pattern.max_completions and pattern.max_completions < overall_pattern_completions) or \
                            pattern.min_completions > overall_pattern_completions:
                continue
            # паттерн на не выполнения события
            if not final_completion_period and pattern.max_completions == 0 and overall_pattern_completions == 0:
                final_completion_period = "Never"

            # если выбрали подходящий период, добавляем в общие данные показатели ретеншена и увеличиваем кол-во юзеров
            if final_completion_period:
                for i in range(0, get_max_retention_day(user_retention) + 1):
                    pattern_analysis[final_completion_period][pattern.name][
                        str(i) + "d"] += user_retention[i]
                pattern_analysis[final_completion_period][
                    pattern.name]["Users"] += 1
                user_found_pattern = True
                # if pattern.name == "1)Построили одну декораций" and final_completion_period == "7-13":
                #   print(report.previous_user.user_id, user_retention)
                #  print(pattern_analysis[final_completion_period][pattern.name])
                # for day in range(0, days_since_install + 1):
                #    print(user_events[day])
        if not user_found_pattern:
            Pattern.users_not_covered_with_pattern.add(
                report.previous_user.user_id)

    # ЦИКЛ ОБРАБОТКИ ДАННЫХ
    while report.get_next_event():

        if report.is_new_user():
            if not classic_retention:
                user_retention = make_rolling_retention(user_retention)
            user_day_events.append(user_session_events)
            user_events[day_in_game] = user_day_events
            flush_user_data()
            user_day_events = []
            user_session_events = []
            previous_day_in_game = 0
            user_retention = [0] * (days_since_install + 1)
            for day in range(0, days_since_install + 1):
                user_events[day] = []

        # определяем день в игре и увеличиваем ртеншен
        day_in_game = report.get_time_since_install("day")
        user_retention[day_in_game] = 1
        # if report.current_user.user_id == "D35DACD8-F579-4A08-9E27-58CF7D193E1F":
        #   print(previous_day_in_game, day_in_game, user_retention)
        if isinstance(report.current_event, CityEventsQuest) and min_quest:
            if CityEventsQuest.compare_quests(report.current_event.quest,
                                              min_quest) == -1:
                continue
        # если новая сессия, добавляем список событий сессии в общие события игрока
        if isinstance(report.current_event, CityEventsInitGameState):
            user_day_events.append(user_session_events)
            # если новый день, добавляем данные по дню в общие данные
            if day_in_game != previous_day_in_game:
                user_events[previous_day_in_game] = user_day_events
                previous_day_in_game = day_in_game
                user_day_events = []
            user_session_events = []
        user_session_events.append(report.current_event)
        # if report.current_user.user_id == "D35DACD8-F579-4A08-9E27-58CF7D193E1F":
        #    print(report.current_event.__class__)
        #   print(day_in_game, user_day_events)
        #  for day in user_events:
        #     print(user_events[day])
    flush_user_data()

    print("Инсталлов:", report.total_users)
    print("Не прошли по паттернам:",
          len(Pattern.users_not_covered_with_pattern))
    # for retention_period in retention_periods:
    #    for pattern_n in [pattern.name for pattern in patterns_list]:
    #       print(retention_period, pattern_n, pattern_analysis[retention_period][pattern_n])

    df = pd.DataFrame(index=[], columns=parameters)

    for retention_period in pattern_analysis.keys():
        for pattern in pattern_analysis[retention_period].keys():
            if pattern_analysis[retention_period][pattern]["Users"] > 0:
                for i in range(0, days_since_install + 1):
                    pattern_analysis[retention_period][pattern][
                        str(i) + "d"] = str(
                            round(
                                pattern_analysis[retention_period][pattern][
                                    str(i) + "d"] * 100 /
                                pattern_analysis[retention_period][pattern]
                                ["Users"], 1)) + "%"

                df = df.append(
                    {
                        "Retention Period": retention_period,
                        "Pattern": pattern,
                        **pattern_analysis[retention_period][pattern]
                    },
                    ignore_index=True)
    df.fillna(0, inplace=True)
    df.sort_values(by=["Retention Period", "Pattern"],
                   ascending=True,
                   inplace=True)
    # Вывод
    print(df.to_string(index=False))
    string = "ClassicRetention" if classic_retention else "RollingRetention"
    writer = pd.ExcelWriter("Retention Pattern Hypothesis/" +
                            Pattern.filename + " " + string + ".xlsx")
    df.to_excel(excel_writer=writer, index=False)
    writer.save()
예제 #2
0
        # Переносим пользовательские данные в общие и обнуляем пользователськие парамтеры
        if report.is_new_user():
            flush_user_data()
            user_accumulative = dict.fromkeys(accumulating_parameters, 0)
            user_transactions = []
            for dt in rrule(DAILY, dtstart=period_start, until=period_end + timedelta(days=days_since_install)):
                user_dau[dt.date()] = None
            ltv = 0
            previous_day_in_game = 0

        publisher = report.current_user.publisher
        source = report.current_user.source

        # Определяем день после установки
        day_in_game = report.get_time_since_install(measure="day")

        # Если день изменился, то заполняем дни от предыдущего до нынешнего предыдущим LTV
        if day_in_game > previous_day_in_game:
            for day in range(previous_day_in_game, day_in_game):
                user_accumulative[str(day) + "d"] = ltv
        if report.current_event.datetime.date() in user_dau.keys() and user_dau[
            report.current_event.datetime.date()] is None:
            user_dau[report.current_event.datetime.date()] = 0

        # Обновляем LTV новой покупкой и Revenue в DAU
        if report.current_event.__class__ in (CityEventsBuyPremiumCoin, Match3BuyPremiumCoin):
            ltv += report.current_event.price
            if report.current_event.datetime.date() in user_dau.keys():
                user_dau[report.current_event.datetime.date()] += report.current_event.price
            # Сохраняем транзации для расчета метрик
예제 #3
0
def new_report(start=1,
               quantity=100,
               days_left=None,
               days_max=3,
               install_app_version="6.5",
               exact=False,
               report_app_version=None):
    if not report_app_version:
        report_app_version = install_app_version
    if not days_left:
        if days_max >= 28:
            days_left = 14
        elif days_max >= 14:
            days_left = 7
        elif days_max >= 7:
            days_left = 3
        else:
            days_left = 999

    last_event_date = Data.get_last_event_date()
    days_max = int(days_max)
    sql = """
        SELECT ios_ifa,ios_ifv, event_name, event_json, event_datetime, app_version_name
        FROM sop_events.events_ios
        WHERE 
        (event_name = "Match3Events" 
            or (event_name = "CityEvent" 
                and (event_json like "%StartGame%"
                    )
                )
        )
        and
        (ios_ifa <>"" or ios_ifv <>"")
        order by ios_ifv, event_datetime,ios_ifv
        """
    max_install_version = None if not exact else install_app_version
    report = Report(sql_events=sql,
                    user_status_check=True,
                    exact=exact,
                    min_app_version=install_app_version,
                    get_installs=True,
                    min_install_version=install_app_version,
                    max_install_version=max_install_version)
    left_par = "Left " + str(days_left) + "+ days"
    levels = get_level_names(start, quantity)
    df = pd.DataFrame(index=levels,
                      columns=[
                          "Started", "Finished", "Start Convertion",
                          "Clean Start", "Clean Finish", left_par,
                          "Difficulty", "Clean Difficulty", "Attempts",
                          "Clean Attempts", "Purchases Sum", "Purchases",
                          "First purchase", "ARPU", "Dust", "Dust on hands"
                      ])
    df = df.fillna(0)

    def start_finish():
        if report.current_event.__class__ is Match3StartGame:
            started_levels.add(current_level)
        elif report.current_event.__class__ in (Match3FinishGame,
                                                Match3CompleteTargets):
            finished_levels.add(current_level)

    def clean_start_finish():
        if report.current_event.__class__ is Match3StartGame:

            if report.current_app_version >= report_app_version:
                if report.current_event.start_bonuses.first or \
                        report.current_event.start_bonuses.second or \
                        report.current_event.start_bonuses.third:
                    return False
                else:
                    df.loc[current_level, "Clean Start"] += 1
                    return True

        elif report.current_event.__class__ is Match3CompleteTargets:
            # проверка на чистую игру на этой версии приложения
            if report.current_app_version >= report_app_version and clean_start:
                # если не использовал молоток, то чисто
                if report.current_event.ingame_bonuses == 0:
                    df.loc[current_level, "Clean Finish"] += 1
                    return True
                else:
                    # если использовал - отбираем чистый старт
                    df.loc[current_level, "Clean Start"] -= 1
                    return False
        return clean_start

    def difficulty():
        if report.current_event.__class__ is Match3StartGame:
            # в сложнотсти считаем все старты уровней
            df.loc[current_level, "Difficulty"] += 1

    def attempts():

        if report.current_event.__class__ is Match3CompleteTargets:
            # количество попыток пройти уровень
            if user_attempts[current_level] == 0:
                user_attempts[current_level] = 1
            level_attempts[current_level].append(user_attempts[current_level])

        elif report.current_event.__class__ is Match3FailGame:
            # Увеличиваем счетчик попыток
            user_attempts[current_level] += 1

    def clean_attempts():

        if report.current_event.__class__ is Match3CompleteTargets:
            if report.current_app_version >= report_app_version:
                if clean_start and report.current_event.ingame_bonuses == 0:
                    # количество чистых попыток пройти уровень
                    if user_clean_attempts[current_level] == 0:
                        user_clean_attempts[current_level] = 1
                    level_clean_attempts[current_level].append(
                        user_clean_attempts[current_level])

        elif report.current_event.__class__ is Match3FailGame:
            if report.current_app_version >= report_app_version:
                if clean_start and report.current_event.ingame_bonuses == 0:
                    user_clean_attempts[current_level] += 1

    def purchases():
        if report.current_event.__class__ is Match3BuyPremiumCoin and report.current_event.status == "Success":
            df.loc[current_level, "Purchases"] += 1
            df.loc[current_level,
                   "Purchases Sum"] += get_price(report.current_event.purchase,
                                                 money="rub")

            # Считаем первые покупки игроков
            if first_purchase:
                df.loc[current_level, "First purchase"] += 1
                return False
        return first_purchase

    def dust():
        if report.current_event.__class__ is Match3FinishGame:

            # Добавляем собранную на уровне пыль
            collected_dust[current_level].append(
                int(report.current_event.game_currency_count))
            # Добавляем пыль на руках у игроков
            user_dust[current_level].append(report.current_user.game_coin)

    # Стартовые параметры
    level_attempts = dict.fromkeys(levels)
    level_clean_attempts = dict.fromkeys(levels)
    user_attempts = dict.fromkeys(levels, 0)
    user_clean_attempts = dict.fromkeys(levels, 0)
    collected_dust = dict.fromkeys(levels)
    user_dust = dict.fromkeys(levels)
    user_clean_start = dict.fromkeys(levels)
    for level in levels:
        level_attempts[level] = []
        level_clean_attempts[level] = []
        collected_dust[level] = []
        user_dust[level] = []
        user_clean_start[level] = []

    started_levels = set()
    finished_levels = set()
    first_purchase = True
    current_level = None
    clean_start = True

    while report.get_next_event():

        # Событие М3 - может смениться уровень
        if report.current_event.__class__ in (Match3BuyPremiumCoin,
                                              Match3CompleteTargets,
                                              Match3FinishGame,
                                              Match3StartGame, Match3FailGame):
            current_level = report.current_event.level_num

        # Если уровень из списка рассматрвиаемых
        if current_level in levels:
            # if report.get_timediff(measure="day")>=1 or report.is_new_user():
            # print(report.current_user.user_id, report.get_time_since_install(), report.get_time_since_last_enter(), report.current_user.entries[-1], report.is_new_user() )

            # Нвоый пользователь
            if (report.is_new_user() and not report.previous_user.is_skipped()
                ) or (report.get_time_since_install() > days_max):
                # print("flush",report.is_new_user(), (report.get_time_since_install() > days_max))
                # если вышло время, то работаем с текущим пользователем
                if report.is_new_user():
                    user = report.previous_user
                    user_str = "previous"
                else:
                    user = report.current_user
                    user_str = "current"

                for level in started_levels:
                    df.loc[level, "Started"] += 1
                for level in finished_levels:
                    df.loc[level, "Finished"] += 1

                # Проверка на отвал
                # При отсутствии максимального дня в игре (для среза выборки) берется макс дата из базы данных.
                if (not days_max and get_timediff(user.last_enter.date(), last_event_date,
                                                         measure="day") >= days_left) or \
                        (days_max and get_timediff(user.last_enter.date(),
                                                          user.install_date + timedelta(
                                                              days=days_max), measure="day") > days_left):
                    if started_levels:
                        for level in list(started_levels - finished_levels):
                            df.loc[level, left_par] += 1

                # сброс личных параметров
                started_levels = set()
                finished_levels = set()
                user_attempts = dict.fromkeys(levels, 0)
                user_clean_attempts = dict.fromkeys(levels, 0)
                first_purchase = True

                if not report.is_new_user() and report.get_time_since_install(
                        user="******") > days_max:
                    report.skip_current_user()
                    continue

            start_finish()
            clean_start = clean_start_finish()
            difficulty()
            attempts()
            clean_attempts()
            first_purchase = purchases()
            dust()

    # Финальные рассчеты
    df["Start Convertion"] = round(df["Started"] * 100 / report.total_users, 1)
    df["Difficulty"] = round(100 - df["Finished"] * 100 / df["Difficulty"], 1)
    df["Clean Difficulty"] = round(
        100 - df["Clean Finish"] * 100 / df["Clean Start"], 1)
    df["ARPU"] = round(df["Purchases Sum"] / df["Started"], 1)
    for level in levels:
        # проверка на выбросы в попытках
        if len(level_attempts[level]) > 0:
            level_attempts[level] = outliers_iqr(level_attempts[level],
                                                 level + " attempts",
                                                 multiplier=10)
            df.loc[level, "Attempts"] = round(
                1 - 1 /
                (sum(level_attempts[level]) / len(level_attempts[level])),
                3) * 100

        if len(level_clean_attempts[level]) > 0:
            level_clean_attempts[level] = outliers_iqr(
                level_clean_attempts[level],
                level + " clean attempts",
                multiplier=4)
            df.loc[level, "Clean Attempts"] = round(
                1 - 1 / (sum(level_clean_attempts[level]) /
                         len(level_clean_attempts[level])), 3) * 100
        # Проверка на выбросы в значениях пыли
        if len(collected_dust[level]) > 0:
            if len(collected_dust[level]) > 10:
                collected_dust[level] = outliers_iqr(collected_dust[level],
                                                     level + " collected dust",
                                                     multiplier=5)
            df.loc[level, "Dust"] = round(
                sum(collected_dust[level]) / float(len(collected_dust[level])),
                1)

        if len(user_dust[level]) > 0:
            if len(user_dust[level]) > 10:
                user_dust[level] = outliers_iqr(data_list=user_dust[level],
                                                where=level + " user dust",
                                                max_outliers=False,
                                                min_outliers=True,
                                                multiplier=15)
                user_dust[level] = outliers_iqr(user_dust[level],
                                                level + " user dust",
                                                multiplier=5)
            df.loc[level, "Dust on hands"] = round(
                sum(user_dust[level]) / float(len(user_dust[level])), 1)

    # Печать
    print("Total users:", report.total_users)
    print("Testers:", len(report.testers))
    print(df.to_string())

    writer = pd.ExcelWriter("Levels " + str(start) + "-" +
                            str(start + quantity - 1) + " " +
                            report_app_version + " " + str(days_max) + "days" +
                            ".xlsx")
    df.to_excel(excel_writer=writer)
    writer.save()
    del report
예제 #4
0
def new_report(short_info=True,
               first_session=False,
               user_status=True,
               detailed_events=list(),
               install_app_version="5.5",
               exact_app_version=True):

    # БАЗА ДАННЫХ
    sql = """
        SELECT ios_ifa, ios_ifv, event_name, event_json, event_datetime, app_version_name
        FROM sop_events.events_ios
        order by ios_ifa, ios_ifv, event_datetime
        """

    report = Report(sql,
                    user_status_check=True,
                    min_app_version=install_app_version,
                    exact=exact_app_version)

    # ФАЙЛЫ ВЫВОДА
    output_file = None
    output_file_returned = None
    output_file_not_returned = None
    if not first_session:
        output_file = open("UserSessions.txt", "w")
    else:
        output_file_returned = open("UserSessions.txt", "w")
        output_file_not_returned = open("UserSessions.txt", "w")

    # ПАРАМЕТРЫ
    skip_user_events = None
    returned_user = False
    output_string = ""
    events_string = ""
    first_session_status = ""

    while report.get_next_event():

        # НОВЫЙ ПОЛЬЗОВАТЕЛЬ
        if report.is_new_user():

            # Создаем текст сессий на вывод
            output_string += "\n\nNew user: "******"\n"
            if report.previous_user.user_id in test_devices_ios:
                output_string += "TESTER\n"
            output_string += events_string
            if user_status:
                if first_session:
                    output_string += first_session_status
                else:
                    output_string += report.previous_user.get_status()

            # Вывод в файлы
            if report.previous_user.first_enter.date() >= datetime.date(
                    2018, 6, 25):
                if not first_session:
                    output_file.write(output_string)
                elif first_session and returned_user:
                    output_file_returned.write(output_string)
                elif first_session and not returned_user:
                    output_file_not_returned.write(output_string)

            # Обнуление параметров
            returned_user = False
            output_string = ""
            events_string = ""
            first_session_status = ""

        # ПЕРВЫЕ СЕССИИ
        if first_session:

            # Вернулся ли пользователь
            if report.get_time_since_install("day") > 0:
                returned_user = True

            # Первая ли сессия
            if report.current_user.first_session:
                first_session_status = report.current_user.get_status()

        # Добавление текста события
        if (first_session
                and report.current_user.first_session) or not first_session:
            if report.current_event.__class__ is CityEventsInitGameState:
                events_string += "---\n"
            if short_info and (not detailed_events or detailed_events
                               and report.current_event.__class__.__name__
                               not in detailed_events):
                events_string += str(
                    report.current_event.datetime
                ) + " " + report.current_event.to_short_string() + "\n"
            else:
                events_string += str(
                    report.current_event.datetime
                ) + " " + report.current_event.to_string() + "\n"
예제 #5
0
def new_report(max_level=121, days=[2, 3], min_app_version=["6.0","6.3"], exact=True, users_last_day=False):
    days = list(map(int, days))
    progress = {}
    users=dict.fromkeys(min_app_version,0)
    # с 1 (0, чтобы точнее были деления) по n (включительно) + 3 финишера (15?)
    levels = list(range(0, max_level + 2))
    for version in min_app_version:
        # БАЗА ДАННЫХ
        sql = """
            SELECT ios_ifa, ios_ifv, event_name, event_json, event_datetime, app_version_name
            FROM sop_events.events_ios
            where event_name = "Match3Events" and ( event_json like "%CompleteTargets%" or event_json like "%FinishGame%")
            order by ios_ifa, ios_ifv,  event_datetime
            """
        report = Report(sql_events=sql, min_app_version=version, exact=exact)

        # ПАРАМЕТРЫ


        finished_levels = set()
        previous_day = None
        progress[version]={}
        for day in days:
            progress[version][day] = [0] * len(levels)

        while report.get_next_event():

            day_in_game = report.get_time_since_install(measure="day")
            if report.is_new_user() or (not users_last_day and (previous_day and day_in_game != previous_day)):
                # НЕ КАЖДОЕ СОБЫТИЕ А ТОЛЬКО КОГДА МЕНЯЕТСЯ ДЕНЬ, либо только когда меняется пользователь, если только для тех, у кого это последний день
                if previous_day in days and finished_levels:
                    max_user_level = int(max(finished_levels))

                    # учитываем финишеры
                    if max_user_level < 9000:
                        progress[version][previous_day][min(max_user_level, max_level)] += 1
                    else:
                        progress[version][previous_day][max_level + 1] += 1

            if report.is_new_user():
                finished_levels = set()
                previous_day = None

            finished_levels.add(report.current_event.level_num)
            previous_day = day_in_game
        print(version,"Total users:", report.total_users)
        users[version]=report.total_users
        del report

    # Рисовка гистограммы
    max_on_screen = 4
    columns = min(2,len(days))
    #columns=2
    lines_wanted = 2
    plots_left = len(days)
    screens = int(math.ceil(plots_left / max_on_screen))
    for i in range(screens):

        lines = lines_wanted if plots_left >= max_on_screen else math.ceil(plots_left / columns)
        fig, axs = plt.subplots(lines, columns, figsize=(18, 8), facecolor='w', edgecolor='k')
        fig.subplots_adjust(hspace=.5, wspace=.2)
        if columns > 1:
            axs = axs.ravel()

        for subplot, day in enumerate(days[i * max_on_screen: min((i + 1) * max_on_screen, len(days))]):
            if columns>1:
                ax=axs[subplot]
            else:
                ax = axs
            for version in min_app_version:
                ax.bar(levels, progress[version][day], alpha=0.5, label=version)
                ax.legend()
                ax.set_xticks(list(chain(range(0, max_level + 1, 10), [max_level + 2])))
                ax.set_xticklabels(list(range(0, max_level + 1, 10)) + ["X"])
                ax.set_title("day " + str(day))
        plt.savefig("progress_histo_absolute" + str(min_app_version) + " (" + str(i) + ").png")
        plt.show()

        plots_left -= columns * lines

        # Рисовка гистограммы
        max_on_screen = 4
        columns = min(2,len(days))
        #columns = 2
        lines_wanted = 2
        plots_left = len(days)
        screens = int(math.ceil(plots_left / max_on_screen))
        for i in range(screens):

            lines = lines_wanted if plots_left >= max_on_screen else math.ceil(plots_left / columns)
            fig, axs = plt.subplots(lines, columns, figsize=(18, 8), facecolor='w', edgecolor='k')
            fig.subplots_adjust(hspace=.5, wspace=.2)

            if columns>1:
                axs = axs.ravel()

            for subplot, day in enumerate(days[i * max_on_screen: min((i + 1) * max_on_screen, len(days))]):
                if columns > 1:
                    ax = axs[subplot]
                else:
                    ax = axs
                for version in min_app_version:
                    ax.bar(levels, [round(user_count*100/users[version],1) for user_count in progress[version][day]], alpha=0.5, label=version)
                    ax.legend()
                    ax.set_xticks(list(chain(range(0, max_level + 1, 10), [max_level + 2])))
                    ax.set_xticklabels(list(range(0, max_level + 1, 10)) + ["X"])
                    ax.set_title("day " + str(day))
            plt.savefig("progress_histo " + str(min_app_version) + " (" + str(i) + ").png")
            plt.show()

            plots_left -= columns * lines