Gather more features about experiences (#467)

* Refactor things to avoid multiple sums and set updates

* Store max and min values too for experiences

* Store touched files and directories too as part of the commit data

* Remove useless default value for files_modified_num

* Use f-string instead of string concatenation for feature names

* Add more features about experiences (average, maximum, minimum, number of elements)

Fixes #370
This commit is contained in:
Marco 2019-05-20 13:14:18 +02:00 коммит произвёл GitHub
Родитель 24c805e64e
Коммит 72dab1a6d8
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 435 добавлений и 173 удалений

Просмотреть файл

@ -438,25 +438,27 @@ class BugExtractor(BaseEstimator, TransformerMixin):
else:
bug["commits"] = []
for f in self.feature_extractors:
res = f(
for feature_extractor in self.feature_extractors:
res = feature_extractor(
bug,
reporter_experience=reporter_experience_map[bug["creator"]],
author_ids=author_ids,
)
feature_extractor_name = feature_extractor.__class__.__name__
if res is None:
continue
if isinstance(res, list):
for item in res:
data[f.__class__.__name__ + "-" + item] = "True"
data[f"{feature_extractor_name}-{item}"] = "True"
continue
if isinstance(res, bool):
res = str(res)
data[f.__class__.__name__] = res
data[feature_extractor_name] = res
reporter_experience_map[bug["creator"]] += 1

Просмотреть файл

@ -6,6 +6,9 @@
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin
EXPERIENCE_TIMESPAN = 90
EXPERIENCE_TIMESPAN_TEXT = f"{EXPERIENCE_TIMESPAN}_days"
class files_modified_num(object):
def __call__(self, commit, **kwargs):
@ -32,49 +35,48 @@ class test_deleted(object):
return commit["test_deleted"]
class author_experience(object):
def __call__(self, commit, **kwargs):
return commit["author_experience"]
class author_experience_90_days(object):
def __call__(self, commit, **kwargs):
return commit["author_experience_90_days"]
def get_exps(exp_type, commit):
suffix = "experience" if exp_type == "reviewer" else "touched_prev"
val_total = commit[f"{exp_type}_{suffix}"]
val_timespan = commit[f"{exp_type}_{suffix}_{EXPERIENCE_TIMESPAN_TEXT}"]
items_key = f"{exp_type}s" if exp_type != "directory" else "directories"
items_num = len(commit[items_key])
return {
"num": items_num,
"sum": val_total["sum"],
"max": val_total["max"],
"min": val_total["min"],
"avg": val_total["sum"] / items_num if items_num > 0 else 0,
f"sum_{EXPERIENCE_TIMESPAN_TEXT}": val_timespan["sum"],
f"max_{EXPERIENCE_TIMESPAN_TEXT}": val_timespan["max"],
f"min_{EXPERIENCE_TIMESPAN_TEXT}": val_timespan["min"],
f"avg_{EXPERIENCE_TIMESPAN_TEXT}": val_timespan["sum"] / items_num
if items_num > 0
else 0,
}
class author_experience(object):
def __call__(self, commit, **kwargs):
return {
"total": commit["author_experience"],
EXPERIENCE_TIMESPAN_TEXT: commit[
f"author_experience_{EXPERIENCE_TIMESPAN_TEXT}"
],
}
class reviewer_experience(object):
def __call__(self, commit, **kwargs):
return commit["reviewer_experience"]
class reviewer_experience_90_days(object):
def __call__(self, commit, **kwargs):
return commit["reviewer_experience_90_days"]
class components_touched_prev(object):
def __call__(self, commit, **kwargs):
return commit["components_touched_prev"]
class components_touched_prev_90_days(object):
def __call__(self, commit, **kwargs):
return commit["components_touched_prev_90_days"]
class files_touched_prev(object):
def __call__(self, commit, **kwargs):
return commit["files_touched_prev"]
class files_touched_prev_90_days(object):
def __call__(self, commit, **kwargs):
return commit["files_touched_prev_90_days"]
class types(object):
def __call__(self, commit, **kwargs):
return commit["types"]
return get_exps("reviewer", commit)
class components(object):
@ -82,9 +84,29 @@ class components(object):
return commit["components"]
class number_of_reviewers(object):
class component_touched_prev(object):
def __call__(self, commit, **kwargs):
return len(commit["reviewers"])
return get_exps("component", commit)
class directories(object):
def __call__(self, commit, **kwargs):
return commit["directories"]
class directory_touched_prev(object):
def __call__(self, commit, **kwargs):
return get_exps("directory", commit)
class file_touched_prev(object):
def __call__(self, commit, **kwargs):
return get_exps("file", commit)
class types(object):
def __call__(self, commit, **kwargs):
return commit["types"]
class CommitExtractor(BaseEstimator, TransformerMixin):
@ -101,21 +123,28 @@ class CommitExtractor(BaseEstimator, TransformerMixin):
for commit in commits:
data = {}
for f in self.feature_extractors:
res = f(commit)
for feature_extractor in self.feature_extractors:
res = feature_extractor(commit)
feature_extractor_name = feature_extractor.__class__.__name__
if res is None:
continue
if isinstance(res, dict):
for key, value in res.items():
data[f"{feature_extractor_name}_{key}"] = value
continue
if isinstance(res, list):
for item in res:
data[f.__class__.__name__ + "-" + item] = "True"
data[f"{feature_extractor_name}-{item}"] = "True"
continue
if isinstance(res, bool):
res = str(res)
data[f.__class__.__name__] = res
data[feature_extractor_name] = res
# TODO: Try simply using all possible fields instead of extracting features manually.

Просмотреть файл

@ -28,16 +28,12 @@ class BackoutModel(CommitModel):
commit_features.deleted(),
commit_features.test_deleted(),
commit_features.author_experience(),
commit_features.author_experience_90_days(),
commit_features.reviewer_experience(),
commit_features.reviewer_experience_90_days(),
commit_features.components_touched_prev(),
commit_features.components_touched_prev_90_days(),
commit_features.files_touched_prev(),
commit_features.files_touched_prev_90_days(),
commit_features.component_touched_prev(),
commit_features.directory_touched_prev(),
commit_features.file_touched_prev(),
commit_features.types(),
commit_features.components(),
commit_features.number_of_reviewers(),
]
cleanup_functions = [

Просмотреть файл

@ -96,6 +96,20 @@ def get_reviewers(commit_description, flag_re=None):
return res
def get_directories(files):
if isinstance(files, str):
files = [files]
directories = set()
for path in files:
path_dirs = (
os.path.dirname(path).split("/", 2)[:2] if os.path.dirname(path) else []
)
if path_dirs:
directories.update([path_dirs[0], "/".join(path_dirs)])
return list(directories)
def get_commits():
return db.read(COMMITS_DB)
@ -141,50 +155,36 @@ def _transform(commit):
"test_added": 0,
"deleted": 0,
"test_deleted": 0,
"files_modified_num": 0,
"types": set(),
"components": list(),
"author_experience": experiences_by_commit["total"]["author"][commit.node],
f"author_experience_{EXPERIENCE_TIMESPAN_TEXT}": experiences_by_commit[
EXPERIENCE_TIMESPAN_TEXT
]["author"][commit.node],
"reviewer_experience": experiences_by_commit["total"]["reviewer"][commit.node],
f"reviewer_experience_{EXPERIENCE_TIMESPAN_TEXT}": experiences_by_commit[
EXPERIENCE_TIMESPAN_TEXT
]["reviewer"][commit.node],
"author_email": commit.author_email.decode("utf-8"),
"components_touched_prev": experiences_by_commit["total"]["component"][
commit.node
],
f"components_touched_prev_{EXPERIENCE_TIMESPAN_TEXT}": experiences_by_commit[
EXPERIENCE_TIMESPAN_TEXT
]["component"][commit.node],
"files_touched_prev": experiences_by_commit["total"]["file"][commit.node],
f"files_touched_prev_{EXPERIENCE_TIMESPAN_TEXT}": experiences_by_commit[
EXPERIENCE_TIMESPAN_TEXT
]["file"][commit.node],
"directories_touched_prev": experiences_by_commit["total"]["directory"][
commit.node
],
f"directories_touched_prev_{EXPERIENCE_TIMESPAN_TEXT}": experiences_by_commit[
EXPERIENCE_TIMESPAN_TEXT
]["directory"][commit.node],
}
for experience_type in ["author", "reviewer", "file", "directory", "component"]:
suffix = (
"experience"
if experience_type in ["author", "reviewer"]
else "touched_prev"
)
obj[f"{experience_type}_{suffix}"] = experiences_by_commit["total"][
experience_type
][commit.node]
obj[
f"{experience_type}_{suffix}_{EXPERIENCE_TIMESPAN_TEXT}"
] = experiences_by_commit[EXPERIENCE_TIMESPAN_TEXT][experience_type][
commit.node
]
sizes = []
patch = HG.export(revs=[commit.node.encode("ascii")], git=True)
patch_data = rs_parsepatch.get_counts(patch)
components = set()
for stats in patch_data:
if stats["binary"]:
obj["types"].add("binary")
continue
path = stats["filename"]
component = path_to_component.get(path)
if component:
components.add(component)
if is_test(path):
obj["test_added"] += stats["added_lines"]
@ -239,7 +239,15 @@ def _transform(commit):
# Covert to a list, as a set is not JSON-serializable.
obj["types"] = list(obj["types"])
obj["components"] = list(components)
obj["components"] = list(
set(
path_to_component[path]
for path in commit.files
if path in path_to_component
)
)
obj["directories"] = get_directories(commit.files)
obj["files"] = commit.files
return obj
@ -319,20 +327,6 @@ def get_revs(hg, date_from=None):
return x.splitlines()
def get_directories(files):
if isinstance(files, str):
files = [files]
directories = set()
for path in files:
path_dirs = (
os.path.dirname(path).split("/", 2)[:2] if os.path.dirname(path) else []
)
if path_dirs:
directories.update([path_dirs[0], "/".join(path_dirs)])
return list(directories)
def calculate_experiences(commits):
print(f"Analyzing experiences from {len(commits)} commits...")
@ -348,39 +342,90 @@ def calculate_experiences(commits):
complex_experiences = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
def update_experiences(experience_type, day, items):
for item in items:
exp = experiences[day][experience_type][item]
total_exps = [experiences[day][experience_type][item] for item in items]
timespan_exps = [
exp - experiences[day - EXPERIENCE_TIMESPAN][experience_type][item]
for exp, item in zip(total_exps, items)
]
experiences_by_commit["total"][experience_type][commit.node] += exp
total_exps_sum = sum(total_exps)
timespan_exps_sum = sum(timespan_exps)
if experience_type == "author":
experiences_by_commit["total"][experience_type][
commit.node
] = total_exps_sum
experiences_by_commit[EXPERIENCE_TIMESPAN_TEXT][experience_type][
commit.node
] += (exp - experiences[day - EXPERIENCE_TIMESPAN][experience_type][item])
] = timespan_exps_sum
else:
experiences_by_commit["total"][experience_type][commit.node] = {
"sum": total_exps_sum,
"max": max(total_exps) if len(total_exps) else 0,
"min": min(total_exps) if len(total_exps) else 0,
}
experiences_by_commit[EXPERIENCE_TIMESPAN_TEXT][experience_type][
commit.node
] = {
"sum": timespan_exps_sum,
"max": max(timespan_exps) if len(timespan_exps) else 0,
"min": min(timespan_exps) if len(timespan_exps) else 0,
}
# We don't want to consider backed out commits when calculating experiences.
if not commit.backedoutby:
# We don't want to consider backed out commits when calculating experiences.
if not commit.backedoutby:
for item in items:
experiences[day][experience_type][item] += 1
def update_complex_experiences(experience_type, day, items, self_node):
all_commits = set()
before_timespan_commits = set()
for item in items:
all_commits.update(complex_experiences[day][experience_type][item])
before_timespan_commits.update(
complex_experiences[day - EXPERIENCE_TIMESPAN][experience_type][item]
def update_complex_experiences(experience_type, day, items):
all_commit_lists = [
complex_experiences[day][experience_type][item] for item in items
]
before_commit_lists = [
complex_experiences[day - EXPERIENCE_TIMESPAN][experience_type][item]
for item in items
]
timespan_commit_lists = [
commit_list[len(before_commit_list) :]
for commit_list, before_commit_list in zip(
all_commit_lists, before_commit_lists
)
]
# We don't want to consider backed out commits when calculating experiences.
if not commit.backedoutby:
complex_experiences[day][experience_type][item].append(commit.node)
all_commits = set(sum(all_commit_lists, []))
timespan_commits = set(sum(timespan_commit_lists, []))
# If a commit changes two files in the same component, we shouldn't increase the exp by two.
all_commits.discard(self_node)
experiences_by_commit["total"][experience_type][commit.node] = len(all_commits)
experiences_by_commit["total"][experience_type][commit.node] = {
"sum": len(all_commits),
"max": max(len(all_commit_list) for all_commit_list in all_commit_lists)
if len(all_commit_lists)
else 0,
"min": min(len(all_commit_list) for all_commit_list in all_commit_lists)
if len(all_commit_lists)
else 0,
}
experiences_by_commit[EXPERIENCE_TIMESPAN_TEXT][experience_type][
commit.node
] = len(all_commits - before_timespan_commits)
] = {
"sum": len(timespan_commits),
"max": max(
len(timespan_commit_list)
for timespan_commit_list in timespan_commit_lists
)
if len(timespan_commit_lists)
else 0,
"min": min(
len(timespan_commit_list)
for timespan_commit_list in timespan_commit_lists
)
if len(timespan_commit_lists)
else 0,
}
# We don't want to consider backed out commits when calculating experiences.
if not commit.backedoutby:
for item in items:
complex_experiences[day][experience_type][item].append(commit.node)
prev_days = 0
@ -428,11 +473,9 @@ def calculate_experiences(commits):
copied_directory
] = complex_experiences[prev_day]["directory"][orig_directory]
update_complex_experiences("file", days, commit.files, commit.node)
update_complex_experiences("file", days, commit.files)
update_complex_experiences(
"directory", days, get_directories(commit.files), commit.node
)
update_complex_experiences("directory", days, get_directories(commit.files))
components = list(
set(
@ -442,7 +485,7 @@ def calculate_experiences(commits):
)
)
update_complex_experiences("component", days, components, commit.node)
update_complex_experiences("component", days, components)
old_days = [
day for day in experiences.keys() if day < days - EXPERIENCE_TIMESPAN

Просмотреть файл

@ -207,58 +207,250 @@ def test_calculate_experiences():
assert repository.experiences_by_commit["90_days"]["author"]["commit5"] == 0
assert repository.experiences_by_commit["90_days"]["author"]["commit6"] == 1
assert repository.experiences_by_commit["total"]["reviewer"]["commit1"] == 0
assert repository.experiences_by_commit["total"]["reviewer"]["commit2"] == 1
assert repository.experiences_by_commit["total"]["reviewer"]["commit3"] == 1
assert repository.experiences_by_commit["total"]["reviewer"]["commit4"] == 4
assert repository.experiences_by_commit["total"]["reviewer"]["commit5"] == 0
assert repository.experiences_by_commit["total"]["reviewer"]["commit6"] == 1
assert repository.experiences_by_commit["total"]["reviewer"]["commit1"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["total"]["reviewer"]["commit2"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["total"]["reviewer"]["commit3"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["total"]["reviewer"]["commit4"] == {
"sum": 4,
"max": 2,
"min": 2,
}
assert repository.experiences_by_commit["total"]["reviewer"]["commit5"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["total"]["reviewer"]["commit6"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit1"] == 0
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit2"] == 1
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit3"] == 1
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit4"] == 0
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit5"] == 0
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit6"] == 1
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit1"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit2"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit3"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit4"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit5"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["90_days"]["reviewer"]["commit6"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["total"]["file"]["commit1"] == 0
assert repository.experiences_by_commit["total"]["file"]["commit2"] == 1
assert repository.experiences_by_commit["total"]["file"]["commit3"] == 1
assert repository.experiences_by_commit["total"]["file"]["commit4"] == 2
assert repository.experiences_by_commit["total"]["file"]["commit5"] == 3
assert repository.experiences_by_commit["total"]["file"]["commit6"] == 4
assert repository.experiences_by_commit["total"]["file"]["commit1"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["total"]["file"]["commit2"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["total"]["file"]["commit3"] == {
"sum": 1,
"max": 1,
"min": 0,
}
assert repository.experiences_by_commit["total"]["file"]["commit4"] == {
"sum": 2,
"max": 2,
"min": 0,
}
assert repository.experiences_by_commit["total"]["file"]["commit5"] == {
"sum": 3,
"max": 3,
"min": 3,
}
assert repository.experiences_by_commit["total"]["file"]["commit6"] == {
"sum": 4,
"max": 4,
"min": 4,
}
assert repository.experiences_by_commit["90_days"]["file"]["commit1"] == 0
assert repository.experiences_by_commit["90_days"]["file"]["commit2"] == 1
assert repository.experiences_by_commit["90_days"]["file"]["commit3"] == 1
assert repository.experiences_by_commit["90_days"]["file"]["commit4"] == 0
assert repository.experiences_by_commit["90_days"]["file"]["commit5"] == 1
assert repository.experiences_by_commit["90_days"]["file"]["commit6"] == 2
assert repository.experiences_by_commit["90_days"]["file"]["commit1"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["90_days"]["file"]["commit2"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["90_days"]["file"]["commit3"] == {
"sum": 1,
"max": 1,
"min": 0,
}
assert repository.experiences_by_commit["90_days"]["file"]["commit4"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["90_days"]["file"]["commit5"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["90_days"]["file"]["commit6"] == {
"sum": 2,
"max": 2,
"min": 2,
}
assert repository.experiences_by_commit["total"]["directory"]["commit1"] == 0
assert repository.experiences_by_commit["total"]["directory"]["commit2"] == 1
assert repository.experiences_by_commit["total"]["directory"]["commit3"] == 2
assert repository.experiences_by_commit["total"]["directory"]["commit4"] == 3
assert repository.experiences_by_commit["total"]["directory"]["commit5"] == 4
assert repository.experiences_by_commit["total"]["directory"]["commit6"] == 5
assert repository.experiences_by_commit["total"]["directory"]["commit1"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["total"]["directory"]["commit2"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["total"]["directory"]["commit3"] == {
"sum": 2,
"max": 2,
"min": 1,
}
assert repository.experiences_by_commit["total"]["directory"]["commit4"] == {
"sum": 3,
"max": 3,
"min": 2,
}
assert repository.experiences_by_commit["total"]["directory"]["commit5"] == {
"sum": 4,
"max": 4,
"min": 4,
}
assert repository.experiences_by_commit["total"]["directory"]["commit6"] == {
"sum": 5,
"max": 5,
"min": 5,
}
assert repository.experiences_by_commit["90_days"]["directory"]["commit1"] == 0
assert repository.experiences_by_commit["90_days"]["directory"]["commit2"] == 1
assert repository.experiences_by_commit["90_days"]["directory"]["commit3"] == 2
assert repository.experiences_by_commit["90_days"]["directory"]["commit4"] == 0
assert repository.experiences_by_commit["90_days"]["directory"]["commit5"] == 1
assert repository.experiences_by_commit["90_days"]["directory"]["commit6"] == 2
assert repository.experiences_by_commit["90_days"]["directory"]["commit1"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["90_days"]["directory"]["commit2"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["90_days"]["directory"]["commit3"] == {
"sum": 2,
"max": 2,
"min": 1,
}
assert repository.experiences_by_commit["90_days"]["directory"]["commit4"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["90_days"]["directory"]["commit5"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["90_days"]["directory"]["commit6"] == {
"sum": 2,
"max": 2,
"min": 2,
}
assert repository.experiences_by_commit["total"]["component"]["commit1"] == 0
assert repository.experiences_by_commit["total"]["component"]["commit2"] == 1
assert repository.experiences_by_commit["total"]["component"]["commit3"] == 1
assert repository.experiences_by_commit["total"]["component"]["commit4"] == 3
assert repository.experiences_by_commit["total"]["component"]["commit5"] == 3
assert repository.experiences_by_commit["total"]["component"]["commit6"] == 4
assert repository.experiences_by_commit["total"]["component"]["commit1"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["total"]["component"]["commit2"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["total"]["component"]["commit3"] == {
"sum": 1,
"max": 1,
"min": 0,
}
assert repository.experiences_by_commit["total"]["component"]["commit4"] == {
"sum": 3,
"max": 2,
"min": 2,
}
assert repository.experiences_by_commit["total"]["component"]["commit5"] == {
"sum": 3,
"max": 3,
"min": 3,
}
assert repository.experiences_by_commit["total"]["component"]["commit6"] == {
"sum": 4,
"max": 4,
"min": 4,
}
assert repository.experiences_by_commit["90_days"]["component"]["commit1"] == 0
assert repository.experiences_by_commit["90_days"]["component"]["commit2"] == 1
assert repository.experiences_by_commit["90_days"]["component"]["commit3"] == 1
assert repository.experiences_by_commit["90_days"]["component"]["commit4"] == 0
assert repository.experiences_by_commit["90_days"]["component"]["commit5"] == 1
assert repository.experiences_by_commit["90_days"]["component"]["commit6"] == 2
assert repository.experiences_by_commit["90_days"]["component"]["commit1"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["90_days"]["component"]["commit2"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["90_days"]["component"]["commit3"] == {
"sum": 1,
"max": 1,
"min": 0,
}
assert repository.experiences_by_commit["90_days"]["component"]["commit4"] == {
"sum": 0,
"max": 0,
"min": 0,
}
assert repository.experiences_by_commit["90_days"]["component"]["commit5"] == {
"sum": 1,
"max": 1,
"min": 1,
}
assert repository.experiences_by_commit["90_days"]["component"]["commit6"] == {
"sum": 2,
"max": 2,
"min": 2,
}