tld_research/1_empirical_analysis/scripts/data_preprocess.ipynb

590 lines
23 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "1345f1c8",
"metadata": {},
"outputs": [],
"source": [
"from tqdm import tqdm\n",
"from pathlib import Path\n",
"import pandas as pd\n",
"import numpy as np\n",
"from pymongo import MongoClient"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "46d49dd9",
"metadata": {},
"outputs": [],
"source": [
"# 软件生态名\n",
"ECO_NAMES = [\n",
" # \"Apache\",\n",
" # \"Jira\",\n",
" # \"Mojang\",\n",
" # \"MongoDB\",\n",
" # \"Qt\",\n",
" \"RedHat\",\n",
"]"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "03cf285e",
"metadata": {},
"outputs": [],
"source": [
"ISSUE_DIR = Path(\"../data/raw/issues\")\n",
"LINK_DIR = Path(\"../data/raw/links\")"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "61e8f96a",
"metadata": {},
"outputs": [],
"source": [
"PRO_ISSUE_DIR = Path(\"../data/processed/issues\")\n",
"PRO_ISSUE_DIR.mkdir(parents=True, exist_ok=True)\n",
"PRO_LINK_DIR = Path(\"../data/processed/links\")\n",
"PRO_LINK_DIR.mkdir(parents=True, exist_ok=True)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "40885839",
"metadata": {},
"outputs": [],
"source": [
"def load_issues(eco_name: str):\n",
" # 加载Issue数据DataFrame\n",
"\n",
" filename = ISSUE_DIR / (eco_name + \".csv\")\n",
" issue_df = pd.read_csv(\n",
" filename, sep=\";\", encoding=\"utf-8\", low_memory=False, index_col=[\"key\"]\n",
" )\n",
" return issue_df"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "1981c072",
"metadata": {},
"outputs": [],
"source": [
"def load_links(eco_name: str):\n",
" # 加载链接数据DataFrame\n",
"\n",
" filename = LINK_DIR / (eco_name + \".csv\")\n",
" link_df = pd.read_csv(filename, sep=\";\", encoding=\"utf-8\", low_memory=False)\n",
" return link_df"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "865088c5",
"metadata": {},
"outputs": [],
"source": [
"def clean_issues(issue_df: pd.DataFrame):\n",
" # 对Issue数据进行清洗\n",
"\n",
" # 把时间数据转换为统一格式\n",
" issue_df[\"created_time\"] = pd.to_datetime(\n",
" issue_df[\"created_time\"], errors=\"coerce\"\n",
" ).apply(lambda x: x.strftime(\"%Y-%m-%d %H:%M:%S\") if pd.notna(x) else np.nan)\n",
"\n",
" return issue_df"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "4447a7d2",
"metadata": {},
"outputs": [],
"source": [
"def clean_links(link_df: pd.DataFrame, issue_df: pd.DataFrame):\n",
" # 对链接数据进行清洗\n",
"\n",
" def column_transform(row):\n",
" return str(sorted(set([row[\"in_issue_key\"], row[\"out_issue_key\"]])))\n",
"\n",
" # 一条一般类型链接会在两个Issue的字段中存在需要清除其中一个副本\n",
" link_df.drop_duplicates(inplace=True)\n",
" print(f\"Left with {len(link_df)} links after removing link duplication\")\n",
"\n",
" # 清除Issue是私有的、无权访问的链接\n",
" condition = (\n",
" link_df[[\"in_issue_key\", \"out_issue_key\"]]\n",
" .isin(issue_df.index.values)\n",
" .all(axis=1)\n",
" )\n",
" link_df = link_df[condition]\n",
" print(f\"Left with {len(link_df)} links after removing half-private issues\")\n",
"\n",
" # 一对Issue间只允许存在一条链接需要删除含有多条链接的Issue对\n",
" # 首先基于'link_key'字段删除重复的Issue对\n",
" # !注意:相同的'link_key'的Issue对之间是可能存在多种类型的链接这会混淆关联关系所以全部清除\n",
" link_df.drop_duplicates(subset=[\"link_key\"], keep=False, inplace=True)\n",
"\n",
" # 其次,以防'link_key'是反过来的比如issue1_issue2和issue2_issue1\n",
" # 所以添加'sorted_issue_keys'字段由链接的两个Issue的key升序组成\n",
" link_df[\"sorted_issue_keys\"] = link_df.apply(column_transform, axis=1)\n",
" # 找出链接的两端Issue的keys相同的行对应的'sorted_issue_keys'字段值\n",
" doublelinks = (\n",
" (link_df[\"sorted_issue_keys\"].value_counts() > 1)\n",
" .rename_axis(\"doubles\")\n",
" .reset_index(name=\"valid\")\n",
" )\n",
" valid_double_keys = set(doublelinks[doublelinks[\"valid\"] == True][\"doubles\"])\n",
"\n",
" # 把重复的'sorted_issue_keys'字段对应的链接类型取出来检查若类型数大于1则清除这些Issue对\n",
" for i in tqdm(valid_double_keys):\n",
" if len(set(link_df[link_df[\"sorted_issue_keys\"] == i][\"link_type\"])) > 1:\n",
" condition = link_df[\"sorted_issue_keys\"] != i\n",
" link_df = link_df[condition]\n",
" print(\n",
" f\"Left with {len(link_df)} links after removing issue-pairs with multiple types of links between them\"\n",
" )\n",
"\n",
" # 最后留下来的链接中仍然可能有重复链接类型的Issue对通过Issue的key对调的方式实现的清除其中一个\n",
" link_df.drop_duplicates(subset=[\"sorted_issue_keys\"], inplace=True)\n",
" print(\n",
" f\"Left with {len(link_df)} links after removing issue-pairs with duplicate same type of links\"\n",
" )\n",
"\n",
" link_df.reset_index(inplace=True, drop=True)\n",
"\n",
" return link_df"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "ab8df842",
"metadata": {},
"outputs": [],
"source": [
"def joined_links(link_df: pd.DataFrame, issue_df: pd.DataFrame):\n",
" # 联合Issue和链接数据\n",
"\n",
" joined_df = link_df.join(issue_df.add_suffix(\"_in\"), on=\"in_issue_key\").join(\n",
" issue_df.add_suffix(\"_out\"), on=\"out_issue_key\"\n",
" )\n",
"\n",
" # !注意补充Subtask类型链接创建时间\n",
" joined_df.loc[\n",
" (joined_df[\"link_type\"] == \"Subtask\") & (joined_df[\"link_created_time\"].isna()),\n",
" \"link_created_time\",\n",
" ] = joined_df[\"created_time_out\"]\n",
"\n",
" return joined_df"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "7a496e37",
"metadata": {},
"outputs": [],
"source": [
"def query_issue_closed_time(eco_name: str, issue_df: pd.DataFrame):\n",
" # 查询Issue的history获取Issue关闭时间\n",
"\n",
" # 定义一个函数,用于处理每个分组\n",
" def handle_group(group):\n",
" # 如果分组内的closed_time全为NaN则保留该分组的第一行\n",
" if group[\"closed_time\"].isna().all():\n",
" return group.iloc[0:1]\n",
" # 否则返回closed_time最大值对应的行\n",
" else:\n",
" return group.loc[[group[\"closed_time\"].idxmax()]]\n",
"\n",
" # 把索引列转换为普通列列名为key\n",
" issue_df = issue_df.reset_index().rename(columns={\"index\": \"key\"})\n",
" # 创建Issue关闭时间列\n",
" issue_df[\"closed_time\"] = None\n",
"\n",
" with MongoClient() as client:\n",
" # 链接数据库\n",
" db = client[\"JiraEcos\"]\n",
" histories_collection = db[eco_name + \"Histories\"]\n",
"\n",
" # 首先取出需要查询的Issue keys并移除重复项\n",
" issue_keys = issue_df[\"key\"].unique().tolist()\n",
"\n",
" # 构造聚合查询管道\n",
" pipeline = [\n",
" {\n",
" # 第一步筛选key在issue_keys列表中的文档\n",
" \"$match\": {\"key\": {\"$in\": issue_keys}}\n",
" },\n",
" {\n",
" # 第二步展开history.items数组\n",
" # 进而返回每个具体更改事件\n",
" \"$unwind\": \"$history.items\"\n",
" },\n",
" {\n",
" # 第三步再次筛选满足特定field值的展开后的文档\n",
" # field字段为status保证更改事件是修改Issue状态\n",
" # toString字段为Closed保证是关闭Issue\n",
" \"$match\": {\n",
" \"history.items.field\": \"status\",\n",
" \"history.items.toString\": \"Closed\",\n",
" }\n",
" },\n",
" {\n",
" # 第四步:指定返回文档的字段\n",
" \"$project\": {\n",
" \"_id\": 0,\n",
" \"query_key\": \"$key\",\n",
" \"created\": \"$history.created\",\n",
" \"field\": \"$history.items.field\",\n",
" \"to\": \"$history.items.to\",\n",
" \"toString\": \"$history.items.toString\",\n",
" }\n",
" },\n",
" ]\n",
"\n",
" # 查询数据库\n",
" query = list(histories_collection.aggregate(pipeline))\n",
" # 转换为DataFrame\n",
" query_df = pd.DataFrame(query)\n",
" # print(query_df.head())\n",
"\n",
" print(\n",
" f\"❕ Test print: {len(issue_df)} issues before merged with query DataFrame\"\n",
" )\n",
"\n",
" # 合并DataFrame基于key与query_key匹配\n",
" merged_df = pd.merge(\n",
" issue_df,\n",
" query_df,\n",
" left_on=\"key\",\n",
" right_on=\"query_key\",\n",
" how=\"left\",\n",
" )\n",
"\n",
" print(\n",
" f\"❕ Test print: {len(merged_df)} issues after merged with query DataFrame\"\n",
" )\n",
"\n",
" # 将merged_df中的created值赋给合并后DataFrame的closed_time字段\n",
" merged_df[\"closed_time\"] = pd.to_datetime(merged_df[\"created\"], errors=\"coerce\")\n",
"\n",
" # 裁切出需要的字段\n",
" issue_df = merged_df[list(issue_df.columns)]\n",
"\n",
" # 最后由于Issue可能会被多次开启与关闭所以保留最后一次关闭时间\n",
" # 按照key进行分组使用groupby和apply处理每个分组\n",
" result_df = (\n",
" issue_df.groupby(\"key\", as_index=False)\n",
" .apply(handle_group)\n",
" .reset_index(drop=True)\n",
" )\n",
" # 统一时间格式\n",
" result_df[\"closed_time\"] = result_df[\"closed_time\"].apply(\n",
" lambda x: x.strftime(\"%Y-%m-%d %H:%M:%S\") if pd.notna(x) else np.nan\n",
" )\n",
" # 把key列重新设置为索引列\n",
" result_df = result_df.set_index(\"key\")\n",
"\n",
" print(f\"❕ Test print: {len(result_df)} issues after processed done\")\n",
"\n",
" return result_df"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "3401742e",
"metadata": {},
"outputs": [],
"source": [
"def query_link_created_time(eco_name: str, link_df: pd.DataFrame):\n",
" # 查询Issue的history获取链接创建时间\n",
"\n",
" # 定义一个函数,用于处理每个分组\n",
" def handle_group(group):\n",
" # 如果分组内的link_created_time全为NaN则保留该分组的第一行\n",
" if group[\"link_created_time\"].isna().all():\n",
" return group.iloc[0:1]\n",
" # 否则返回link_created_time最大值对应的行\n",
" else:\n",
" return group.loc[[group[\"link_created_time\"].idxmax()]]\n",
"\n",
" # 裁切出需要的字段\n",
" link_df = link_df[[\"link_type\", \"in_issue_key\", \"out_issue_key\"]]\n",
" # 创建链接创建时间列\n",
" link_df[\"link_created_time\"] = None\n",
"\n",
" with MongoClient() as client:\n",
" # 链接数据库\n",
" db = client[\"JiraEcos\"]\n",
" histories_collection = db[eco_name + \"Histories\"]\n",
"\n",
" # 首先取出需要查询的Issue keys并移除重复项\n",
" out_issue_keys = link_df[\"out_issue_key\"].tolist()\n",
" out_issue_keys = list(set(out_issue_keys))\n",
"\n",
" # 构造聚合查询管道\n",
" pipeline = [\n",
" {\n",
" # 第一步筛选key在out_issue_keys列表中的文档\n",
" # 从而保证只取出有链接的Issue的更改事件\n",
" \"$match\": {\"key\": {\"$in\": out_issue_keys}}\n",
" },\n",
" {\n",
" # 第二步展开history.items数组\n",
" # 进而返回每个具体更改事件\n",
" \"$unwind\": \"$history.items\"\n",
" },\n",
" {\n",
" # 第三步再次筛选满足特定field值的展开后的文档\n",
" # field保证更改事件是链接创建或删除\n",
" # to或toString字段不为空保证是创建链接的事件而不是删除\n",
" \"$match\": {\n",
" \"history.items.field\": {\n",
" \"$in\": [\"Link\", \"Epic Child\", \"Parent\", \"Parent Issue\"]\n",
" },\n",
" \"$or\": [\n",
" {\"history.items.to\": {\"$ne\": None}},\n",
" {\"history.items.toString\": {\"$ne\": None}},\n",
" ],\n",
" }\n",
" },\n",
" {\n",
" # 第四步:指定返回文档的格式\n",
" # target_key根据链接类型获取to或toString字段信息\n",
" \"$project\": {\n",
" \"_id\": 0,\n",
" \"key\": 1,\n",
" \"created\": \"$history.created\",\n",
" \"field\": \"$history.items.field\",\n",
" \"to\": \"$history.items.to\",\n",
" \"toString\": \"$history.items.toString\",\n",
" \"target_key\": {\n",
" \"$cond\": {\n",
" \"if\": {\"$eq\": [\"$history.items.field\", \"Link\"]},\n",
" \"then\": \"$history.items.to\",\n",
" \"else\": \"$history.items.toString\",\n",
" }\n",
" },\n",
" }\n",
" },\n",
" ]\n",
"\n",
" # 查询数据库\n",
" query = list(histories_collection.aggregate(pipeline))\n",
" # 转换为DataFrame\n",
" query_df = pd.DataFrame(query)\n",
" # print(query_df.head())\n",
"\n",
" # 合并DataFrame基于out_issue_key和key匹配in_issue_key和target_key匹配\n",
" merged_df = pd.merge(\n",
" link_df,\n",
" query_df,\n",
" left_on=[\"out_issue_key\", \"in_issue_key\"],\n",
" right_on=[\"key\", \"target_key\"],\n",
" how=\"left\",\n",
" )\n",
"\n",
" # 将merged_df中的created值赋给合并后DataFrame的link_created_time字段\n",
" merged_df[\"link_created_time\"] = merged_df[\"created\"]\n",
"\n",
" # 裁切出需要的字段\n",
" link_df = merged_df[\n",
" [\"link_type\", \"in_issue_key\", \"out_issue_key\", \"link_created_time\"]\n",
" ]\n",
"\n",
" # 最后由于in_issue和out_issue之间可能会发生相同类型链接的多次创建活动\n",
" # 所以,保留最后一次链接创建时间\n",
" # 转换link_created_time为datetime以确保比较的准确性\n",
" link_df[\"link_created_time\"] = pd.to_datetime(\n",
" link_df[\"link_created_time\"], errors=\"coerce\"\n",
" )\n",
"\n",
" # 按照除link_created_time以外的所有字段进行分组使用groupby和apply处理每个分组\n",
" result_df = (\n",
" link_df.groupby(\n",
" [\"link_type\", \"in_issue_key\", \"out_issue_key\"], as_index=False\n",
" )\n",
" .apply(handle_group)\n",
" .reset_index(drop=True)\n",
" )\n",
" # 统一时间格式\n",
" result_df[\"link_created_time\"] = result_df[\"link_created_time\"].apply(\n",
" lambda x: x.strftime(\"%Y-%m-%d %H:%M:%S\") if pd.notna(x) else np.nan\n",
" )\n",
"\n",
" return result_df"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "b42827a1",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"✔ Loaded 502297 raw issues and 405070 raw links for RedHat\n",
"Left with 268935 links after removing link duplication\n",
"Left with 249733 links after removing half-private issues\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/tmp/ipykernel_523178/4239802332.py:23: SettingWithCopyWarning: \n",
"A value is trying to be set on a copy of a slice from a DataFrame\n",
"\n",
"See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n",
" link_df.drop_duplicates(subset=[\"link_key\"], keep=False, inplace=True)\n",
"/tmp/ipykernel_523178/4239802332.py:27: SettingWithCopyWarning: \n",
"A value is trying to be set on a copy of a slice from a DataFrame.\n",
"Try using .loc[row_indexer,col_indexer] = value instead\n",
"\n",
"See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n",
" link_df[\"sorted_issue_keys\"] = link_df.apply(column_transform, axis=1)\n",
"100%|██████████| 4004/4004 [01:48<00:00, 36.81it/s]\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Left with 238349 links after removing issue-pairs with multiple types of links between them\n",
"Left with 238053 links after removing issue-pairs with duplicate same type of links\n",
"✔ Cleaned 502297 issues for RedHat\n",
"✔ Cleaned 238053 links for RedHat\n",
"✔ Link type distribution:\n",
"link_type\n",
"Epic 67799\n",
"Subtask 45020\n",
"Related 44222\n",
"Cloners 29629\n",
"Blocks 21106\n",
"Incorporates 12847\n",
"Duplicate 7080\n",
"Causality 4122\n",
"Depend 2849\n",
"Document 1652\n",
"Issue split 694\n",
"Account 568\n",
"Triggers 465\n",
"Name: count, dtype: int64\n",
"❕ Test print: 502297 issues before merged with query DataFrame\n",
"❕ Test print: 561112 issues after merged with query DataFrame\n",
"❕ Test print: 502297 issues after processed done\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/tmp/ipykernel_523178/834440382.py:16: SettingWithCopyWarning: \n",
"A value is trying to be set on a copy of a slice from a DataFrame.\n",
"Try using .loc[row_indexer,col_indexer] = value instead\n",
"\n",
"See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n",
" link_df[\"link_created_time\"] = None\n",
"/tmp/ipykernel_523178/834440382.py:100: SettingWithCopyWarning: \n",
"A value is trying to be set on a copy of a slice from a DataFrame.\n",
"Try using .loc[row_indexer,col_indexer] = value instead\n",
"\n",
"See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n",
" link_df[\"link_created_time\"] = pd.to_datetime(\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"✅ ----------------------------\n",
"\n"
]
}
],
"source": [
"for eco_name in ECO_NAMES:\n",
" # 加载Issue和链接数据DataFrame\n",
" issue_df = load_issues(eco_name)\n",
" link_df = load_links(eco_name)\n",
" print(\n",
" f\"✔ Loaded {len(issue_df)} raw issues and {len(link_df)} raw links for {eco_name}\"\n",
" )\n",
"\n",
" # 对Issue和链接数据进行清理\n",
" issue_df = clean_issues(issue_df)\n",
" link_df = clean_links(link_df, issue_df)\n",
" print(f\"✔ Cleaned {len(issue_df)} issues for {eco_name}\")\n",
" print(f\"✔ Cleaned {len(link_df)} links for {eco_name}\")\n",
"\n",
" # 打印不同链接类型分布\n",
" print(\"✔ Link type distribution:\")\n",
" print(link_df[\"link_type\"].value_counts())\n",
"\n",
" # 添加Issue关闭时间\n",
" issue_df = query_issue_closed_time(eco_name, issue_df)\n",
"\n",
" # 添加链接创建时间\n",
" link_df = query_link_created_time(eco_name, link_df)\n",
"\n",
" # 联合Issue和链接数据\n",
" link_df = joined_links(link_df, issue_df)\n",
"\n",
" # 保存清理后的Issue和链接数据\n",
" issue_df.to_csv(\n",
" PRO_ISSUE_DIR / (eco_name + \".csv\"),\n",
" sep=\";\",\n",
" index=True, #! issue_df的key被设置为了索引列所以这里需要保存\n",
" )\n",
" link_df.to_csv(\n",
" PRO_LINK_DIR / (eco_name + \".csv\"),\n",
" sep=\";\",\n",
" index=False,\n",
" )\n",
"\n",
" print(\"✅ ----------------------------\\n\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.18"
}
},
"nbformat": 4,
"nbformat_minor": 5
}