Upload 27 files
Browse files- 2025-04-15_Introduction_to_Artificial_Intelligence.pdf +0 -0
- analysis_concept_frequencies.parquet +3 -0
- analysis_concept_similarities.parquet +3 -0
- analysis_network_results.parquet +3 -0
- bestTest.png +0 -0
- concept_embeddings.pkl +3 -0
- concept_network.pkl +3 -0
- concept_network_visualization.html +374 -0
- concept_similarities.parquet +3 -0
- concepts.parquet +3 -0
- documents.parquet +3 -0
- extractor.py +197 -0
- loaders.py +136 -0
- mentions.parquet +3 -0
- network_analysis.py +154 -0
- network_builder.py +118 -0
- plotting.py +155 -0
- relationships.parquet +3 -0
- requirements.txt +2 -0
- reset_status.py +24 -0
- run_analysis.py +125 -0
- run_extractor.py +15 -0
- run_loader.py +14 -0
- similarity.py +170 -0
- storage.py +150 -0
- temporal.py +164 -0
- test1.png +0 -0
2025-04-15_Introduction_to_Artificial_Intelligence.pdf
ADDED
|
Binary file (53.3 kB). View file
|
|
|
analysis_concept_frequencies.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:31cb3e93a0c0c1eb3f65ba695a75b03bdd2b67f80fefd9b7810497ec50100d42
|
| 3 |
+
size 3618
|
analysis_concept_similarities.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0cf17175e4b6a3fac9648e20938ce6c90e4d90dbdc1c71186846a17eff77b45a
|
| 3 |
+
size 4846
|
analysis_network_results.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:98e8ca5669f2a601c184388dda9b34d870ef5a35e69336a4bc23adff7ffc14c2
|
| 3 |
+
size 5022
|
bestTest.png
ADDED
|
concept_embeddings.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aa1f484881118bfd68ca830e070e85c0e380a1be6bbef1c637ea17303bc4d167
|
| 3 |
+
size 17716
|
concept_network.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:acc729f59905a3394915ccb0d731ba2bc1a25b2a16f7cd524cb1cdfa451420db
|
| 3 |
+
size 2336
|
concept_network_visualization.html
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<html>
|
| 2 |
+
<head>
|
| 3 |
+
<meta charset="utf-8">
|
| 4 |
+
|
| 5 |
+
<script>function neighbourhoodHighlight(params) {
|
| 6 |
+
// console.log("in nieghbourhoodhighlight");
|
| 7 |
+
allNodes = nodes.get({ returnType: "Object" });
|
| 8 |
+
// originalNodes = JSON.parse(JSON.stringify(allNodes));
|
| 9 |
+
// if something is selected:
|
| 10 |
+
if (params.nodes.length > 0) {
|
| 11 |
+
highlightActive = true;
|
| 12 |
+
var i, j;
|
| 13 |
+
var selectedNode = params.nodes[0];
|
| 14 |
+
var degrees = 2;
|
| 15 |
+
|
| 16 |
+
// mark all nodes as hard to read.
|
| 17 |
+
for (let nodeId in allNodes) {
|
| 18 |
+
// nodeColors[nodeId] = allNodes[nodeId].color;
|
| 19 |
+
allNodes[nodeId].color = "rgba(200,200,200,0.5)";
|
| 20 |
+
if (allNodes[nodeId].hiddenLabel === undefined) {
|
| 21 |
+
allNodes[nodeId].hiddenLabel = allNodes[nodeId].label;
|
| 22 |
+
allNodes[nodeId].label = undefined;
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
var connectedNodes = network.getConnectedNodes(selectedNode);
|
| 26 |
+
var allConnectedNodes = [];
|
| 27 |
+
|
| 28 |
+
// get the second degree nodes
|
| 29 |
+
for (i = 1; i < degrees; i++) {
|
| 30 |
+
for (j = 0; j < connectedNodes.length; j++) {
|
| 31 |
+
allConnectedNodes = allConnectedNodes.concat(
|
| 32 |
+
network.getConnectedNodes(connectedNodes[j])
|
| 33 |
+
);
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// all second degree nodes get a different color and their label back
|
| 38 |
+
for (i = 0; i < allConnectedNodes.length; i++) {
|
| 39 |
+
// allNodes[allConnectedNodes[i]].color = "pink";
|
| 40 |
+
allNodes[allConnectedNodes[i]].color = "rgba(150,150,150,0.75)";
|
| 41 |
+
if (allNodes[allConnectedNodes[i]].hiddenLabel !== undefined) {
|
| 42 |
+
allNodes[allConnectedNodes[i]].label =
|
| 43 |
+
allNodes[allConnectedNodes[i]].hiddenLabel;
|
| 44 |
+
allNodes[allConnectedNodes[i]].hiddenLabel = undefined;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// all first degree nodes get their own color and their label back
|
| 49 |
+
for (i = 0; i < connectedNodes.length; i++) {
|
| 50 |
+
// allNodes[connectedNodes[i]].color = undefined;
|
| 51 |
+
allNodes[connectedNodes[i]].color = nodeColors[connectedNodes[i]];
|
| 52 |
+
if (allNodes[connectedNodes[i]].hiddenLabel !== undefined) {
|
| 53 |
+
allNodes[connectedNodes[i]].label =
|
| 54 |
+
allNodes[connectedNodes[i]].hiddenLabel;
|
| 55 |
+
allNodes[connectedNodes[i]].hiddenLabel = undefined;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// the main node gets its own color and its label back.
|
| 60 |
+
// allNodes[selectedNode].color = undefined;
|
| 61 |
+
allNodes[selectedNode].color = nodeColors[selectedNode];
|
| 62 |
+
if (allNodes[selectedNode].hiddenLabel !== undefined) {
|
| 63 |
+
allNodes[selectedNode].label = allNodes[selectedNode].hiddenLabel;
|
| 64 |
+
allNodes[selectedNode].hiddenLabel = undefined;
|
| 65 |
+
}
|
| 66 |
+
} else if (highlightActive === true) {
|
| 67 |
+
// console.log("highlightActive was true");
|
| 68 |
+
// reset all nodes
|
| 69 |
+
for (let nodeId in allNodes) {
|
| 70 |
+
// allNodes[nodeId].color = "purple";
|
| 71 |
+
allNodes[nodeId].color = nodeColors[nodeId];
|
| 72 |
+
// delete allNodes[nodeId].color;
|
| 73 |
+
if (allNodes[nodeId].hiddenLabel !== undefined) {
|
| 74 |
+
allNodes[nodeId].label = allNodes[nodeId].hiddenLabel;
|
| 75 |
+
allNodes[nodeId].hiddenLabel = undefined;
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
highlightActive = false;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// transform the object into an array
|
| 82 |
+
var updateArray = [];
|
| 83 |
+
if (params.nodes.length > 0) {
|
| 84 |
+
for (let nodeId in allNodes) {
|
| 85 |
+
if (allNodes.hasOwnProperty(nodeId)) {
|
| 86 |
+
// console.log(allNodes[nodeId]);
|
| 87 |
+
updateArray.push(allNodes[nodeId]);
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
nodes.update(updateArray);
|
| 91 |
+
} else {
|
| 92 |
+
// console.log("Nothing was selected");
|
| 93 |
+
for (let nodeId in allNodes) {
|
| 94 |
+
if (allNodes.hasOwnProperty(nodeId)) {
|
| 95 |
+
// console.log(allNodes[nodeId]);
|
| 96 |
+
// allNodes[nodeId].color = {};
|
| 97 |
+
updateArray.push(allNodes[nodeId]);
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
nodes.update(updateArray);
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
function filterHighlight(params) {
|
| 105 |
+
allNodes = nodes.get({ returnType: "Object" });
|
| 106 |
+
// if something is selected:
|
| 107 |
+
if (params.nodes.length > 0) {
|
| 108 |
+
filterActive = true;
|
| 109 |
+
let selectedNodes = params.nodes;
|
| 110 |
+
|
| 111 |
+
// hiding all nodes and saving the label
|
| 112 |
+
for (let nodeId in allNodes) {
|
| 113 |
+
allNodes[nodeId].hidden = true;
|
| 114 |
+
if (allNodes[nodeId].savedLabel === undefined) {
|
| 115 |
+
allNodes[nodeId].savedLabel = allNodes[nodeId].label;
|
| 116 |
+
allNodes[nodeId].label = undefined;
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
for (let i=0; i < selectedNodes.length; i++) {
|
| 121 |
+
allNodes[selectedNodes[i]].hidden = false;
|
| 122 |
+
if (allNodes[selectedNodes[i]].savedLabel !== undefined) {
|
| 123 |
+
allNodes[selectedNodes[i]].label = allNodes[selectedNodes[i]].savedLabel;
|
| 124 |
+
allNodes[selectedNodes[i]].savedLabel = undefined;
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
} else if (filterActive === true) {
|
| 129 |
+
// reset all nodes
|
| 130 |
+
for (let nodeId in allNodes) {
|
| 131 |
+
allNodes[nodeId].hidden = false;
|
| 132 |
+
if (allNodes[nodeId].savedLabel !== undefined) {
|
| 133 |
+
allNodes[nodeId].label = allNodes[nodeId].savedLabel;
|
| 134 |
+
allNodes[nodeId].savedLabel = undefined;
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
filterActive = false;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// transform the object into an array
|
| 141 |
+
var updateArray = [];
|
| 142 |
+
if (params.nodes.length > 0) {
|
| 143 |
+
for (let nodeId in allNodes) {
|
| 144 |
+
if (allNodes.hasOwnProperty(nodeId)) {
|
| 145 |
+
updateArray.push(allNodes[nodeId]);
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
nodes.update(updateArray);
|
| 149 |
+
} else {
|
| 150 |
+
for (let nodeId in allNodes) {
|
| 151 |
+
if (allNodes.hasOwnProperty(nodeId)) {
|
| 152 |
+
updateArray.push(allNodes[nodeId]);
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
nodes.update(updateArray);
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
function selectNode(nodes) {
|
| 160 |
+
network.selectNodes(nodes);
|
| 161 |
+
neighbourhoodHighlight({ nodes: nodes });
|
| 162 |
+
return nodes;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
function selectNodes(nodes) {
|
| 166 |
+
network.selectNodes(nodes);
|
| 167 |
+
filterHighlight({nodes: nodes});
|
| 168 |
+
return nodes;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
function highlightFilter(filter) {
|
| 172 |
+
let selectedNodes = []
|
| 173 |
+
let selectedProp = filter['property']
|
| 174 |
+
if (filter['item'] === 'node') {
|
| 175 |
+
let allNodes = nodes.get({ returnType: "Object" });
|
| 176 |
+
for (let nodeId in allNodes) {
|
| 177 |
+
if (allNodes[nodeId][selectedProp] && filter['value'].includes((allNodes[nodeId][selectedProp]).toString())) {
|
| 178 |
+
selectedNodes.push(nodeId)
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
else if (filter['item'] === 'edge'){
|
| 183 |
+
let allEdges = edges.get({returnType: 'object'});
|
| 184 |
+
// check if the selected property exists for selected edge and select the nodes connected to the edge
|
| 185 |
+
for (let edge in allEdges) {
|
| 186 |
+
if (allEdges[edge][selectedProp] && filter['value'].includes((allEdges[edge][selectedProp]).toString())) {
|
| 187 |
+
selectedNodes.push(allEdges[edge]['from'])
|
| 188 |
+
selectedNodes.push(allEdges[edge]['to'])
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
selectNodes(selectedNodes)
|
| 193 |
+
}</script>
|
| 194 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/vis-network/9.1.2/dist/dist/vis-network.min.css" integrity="sha512-WgxfT5LWjfszlPHXRmBWHkV2eceiWTOBvrKCNbdgDYTHrT2AeLCGbF4sZlZw3UMN3WtL0tGUoIAKsu8mllg/XA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
| 195 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis-network/9.1.2/dist/vis-network.min.js" integrity="sha512-LnvoEWDFrqGHlHmDD2101OrLcbsfkrzoSpvtSQtxK3RMnRV0eOkhhBN2dXHKRrUU8p2DGRTk35n4O8nWSVe1mQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
<center>
|
| 205 |
+
<h1>ChronoSense Konsept A�� (Metriklerle)</h1>
|
| 206 |
+
</center>
|
| 207 |
+
|
| 208 |
+
<!-- <link rel="stylesheet" href="../node_modules/vis/dist/vis.min.css" type="text/css" />
|
| 209 |
+
<script type="text/javascript" src="../node_modules/vis/dist/vis.js"> </script>-->
|
| 210 |
+
<link
|
| 211 |
+
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css"
|
| 212 |
+
rel="stylesheet"
|
| 213 |
+
integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6"
|
| 214 |
+
crossorigin="anonymous"
|
| 215 |
+
/>
|
| 216 |
+
<script
|
| 217 |
+
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js"
|
| 218 |
+
integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf"
|
| 219 |
+
crossorigin="anonymous"
|
| 220 |
+
></script>
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
<center>
|
| 224 |
+
<h1>ChronoSense Konsept A�� (Metriklerle)</h1>
|
| 225 |
+
</center>
|
| 226 |
+
<style type="text/css">
|
| 227 |
+
|
| 228 |
+
#mynetwork {
|
| 229 |
+
width: 100%;
|
| 230 |
+
height: 800px;
|
| 231 |
+
background-color: #ffffff;
|
| 232 |
+
border: 1px solid lightgray;
|
| 233 |
+
position: relative;
|
| 234 |
+
float: left;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
#config {
|
| 241 |
+
float: left;
|
| 242 |
+
width: 400px;
|
| 243 |
+
height: 600px;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
</style>
|
| 249 |
+
</head>
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
<body>
|
| 253 |
+
<div class="card" style="width: 100%">
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
<div id="mynetwork" class="card-body"></div>
|
| 257 |
+
</div>
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
<div id="config"></div>
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
<script type="text/javascript">
|
| 265 |
+
|
| 266 |
+
// initialize global variables.
|
| 267 |
+
var edges;
|
| 268 |
+
var nodes;
|
| 269 |
+
var allNodes;
|
| 270 |
+
var allEdges;
|
| 271 |
+
var nodeColors;
|
| 272 |
+
var originalNodes;
|
| 273 |
+
var network;
|
| 274 |
+
var container;
|
| 275 |
+
var options, data;
|
| 276 |
+
var filter = {
|
| 277 |
+
item : '',
|
| 278 |
+
property : '',
|
| 279 |
+
value : []
|
| 280 |
+
};
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
// This method is responsible for drawing the graph, returns the drawn network
|
| 287 |
+
function drawGraph() {
|
| 288 |
+
var container = document.getElementById('mynetwork');
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
// parsing and collecting nodes and edges from the python
|
| 293 |
+
nodes = new vis.DataSet([{"color": "#ff7f0e", "id": "b8566bb8-f043-45d0-8442-c8f3e729a626", "label": "ai", "shape": "dot", "size": 40.0, "title": "ID: b8566bb8-f043-45d0-8442-c8f3e729a626\u003cbr\u003eName: ai\u003cbr\u003edegree_centrality: 0.300\u003cbr\u003ecommunity_id: 1"}, {"color": "#2ca02c", "id": "acdb0052-9fb5-4a61-8ce3-4fa9188ccd68", "label": "unsupervised learning: finding", "shape": "dot", "size": 40.0, "title": "ID: acdb0052-9fb5-4a61-8ce3-4fa9188ccd68\u003cbr\u003eName: unsupervised learning: finding\u003cbr\u003edegree_centrality: 0.300\u003cbr\u003ecommunity_id: 2"}, {"color": "#2ca02c", "id": "c9a071e5-358b-460f-897d-5a0d68b4dc91", "label": "reinforcement learning", "shape": "dot", "size": 40.0, "title": "ID: c9a071e5-358b-460f-897d-5a0d68b4dc91\u003cbr\u003eName: reinforcement learning\u003cbr\u003edegree_centrality: 0.300\u003cbr\u003ecommunity_id: 2"}, {"color": "#d62728", "id": "8bcb0007-453a-45a8-b0f5-ccb49fc963be", "label": "deep learning", "shape": "dot", "size": 10, "title": "ID: 8bcb0007-453a-45a8-b0f5-ccb49fc963be\u003cbr\u003eName: deep learning\u003cbr\u003edegree_centrality: 0.000\u003cbr\u003ecommunity_id: 3"}, {"color": "#1f77b4", "id": "544a779d-f9b6-4720-bfdf-80a26574d819", "label": "nlp", "shape": "dot", "size": 20.0, "title": "ID: 544a779d-f9b6-4720-bfdf-80a26574d819\u003cbr\u003eName: nlp\u003cbr\u003edegree_centrality: 0.100\u003cbr\u003ecommunity_id: 0"}, {"color": "#ff7f0e", "id": "1b3a4eb6-a80f-4098-b98e-2ca50ecbdbc6", "label": "chatbots", "shape": "dot", "size": 30.0, "title": "ID: 1b3a4eb6-a80f-4098-b98e-2ca50ecbdbc6\u003cbr\u003eName: chatbots\u003cbr\u003edegree_centrality: 0.200\u003cbr\u003ecommunity_id: 1"}, {"color": "#2ca02c", "id": "ffec4610-96c3-4a0f-a592-573143619a30", "label": "supervised learning", "shape": "dot", "size": 40.0, "title": "ID: ffec4610-96c3-4a0f-a592-573143619a30\u003cbr\u003eName: supervised learning\u003cbr\u003edegree_centrality: 0.300\u003cbr\u003ecommunity_id: 2"}, {"color": "#2ca02c", "id": "c7b69b48-9fea-45de-868d-27f935a7b2b7", "label": "labeled data unsupervised learning", "shape": "dot", "size": 40.0, "title": "ID: c7b69b48-9fea-45de-868d-27f935a7b2b7\u003cbr\u003eName: labeled data unsupervised learning\u003cbr\u003edegree_centrality: 0.300\u003cbr\u003ecommunity_id: 2"}, {"color": "#1f77b4", "id": "18f1cc03-9cfc-40c8-aa86-279a700a7f58", "label": "this approach", "shape": "dot", "size": 20.0, "title": "ID: 18f1cc03-9cfc-40c8-aa86-279a700a7f58\u003cbr\u003eName: this approach\u003cbr\u003edegree_centrality: 0.100\u003cbr\u003ecommunity_id: 0"}, {"color": "#ff7f0e", "id": "78b888f4-c0bf-492e-b514-3da1f628797d", "label": "gpt-4", "shape": "dot", "size": 30.0, "title": "ID: 78b888f4-c0bf-492e-b514-3da1f628797d\u003cbr\u003eName: gpt-4\u003cbr\u003edegree_centrality: 0.200\u003cbr\u003ecommunity_id: 1"}, {"color": "#ff7f0e", "id": "903e5742-9937-42c1-917d-ea7ff7ac449e", "label": "these models", "shape": "dot", "size": 20.0, "title": "ID: 903e5742-9937-42c1-917d-ea7ff7ac449e\u003cbr\u003eName: these models\u003cbr\u003edegree_centrality: 0.100\u003cbr\u003ecommunity_id: 1"}]);
|
| 294 |
+
edges = new vis.DataSet([{"color": "#9370DB", "from": "b8566bb8-f043-45d0-8442-c8f3e729a626", "title": "Type: combined\u003cbr\u003eRelation: RELATED_TO\u003cbr\u003eSimilarity: 0.648", "to": "1b3a4eb6-a80f-4098-b98e-2ca50ecbdbc6", "value": 0.647527813911438}, {"color": "#9370DB", "from": "b8566bb8-f043-45d0-8442-c8f3e729a626", "title": "Type: combined\u003cbr\u003eRelation: RELATED_TO\u003cbr\u003eSimilarity: 0.648", "to": "78b888f4-c0bf-492e-b514-3da1f628797d", "value": 0.647527813911438}, {"color": "#4682B4", "from": "b8566bb8-f043-45d0-8442-c8f3e729a626", "title": "Type: similarity\u003cbr\u003eSimilarity: 0.627", "to": "903e5742-9937-42c1-917d-ea7ff7ac449e", "value": 0.6268218755722046}, {"color": "#FF6347", "from": "acdb0052-9fb5-4a61-8ce3-4fa9188ccd68", "title": "Type: extracted\u003cbr\u003eRelation: RELATED_TO", "to": "c9a071e5-358b-460f-897d-5a0d68b4dc91", "value": 0.8}, {"color": "#FF6347", "from": "acdb0052-9fb5-4a61-8ce3-4fa9188ccd68", "title": "Type: extracted\u003cbr\u003eRelation: RELATED_TO", "to": "ffec4610-96c3-4a0f-a592-573143619a30", "value": 0.8}, {"color": "#FF6347", "from": "acdb0052-9fb5-4a61-8ce3-4fa9188ccd68", "title": "Type: extracted\u003cbr\u003eRelation: RELATED_TO", "to": "c7b69b48-9fea-45de-868d-27f935a7b2b7", "value": 0.8}, {"color": "#FF6347", "from": "c9a071e5-358b-460f-897d-5a0d68b4dc91", "title": "Type: extracted\u003cbr\u003eRelation: RELATED_TO", "to": "ffec4610-96c3-4a0f-a592-573143619a30", "value": 0.8}, {"color": "#FF6347", "from": "c9a071e5-358b-460f-897d-5a0d68b4dc91", "title": "Type: extracted\u003cbr\u003eRelation: RELATED_TO", "to": "c7b69b48-9fea-45de-868d-27f935a7b2b7", "value": 0.8}, {"color": "#FF6347", "from": "544a779d-f9b6-4720-bfdf-80a26574d819", "title": "Type: extracted\u003cbr\u003eRelation: RELATED_TO", "to": "18f1cc03-9cfc-40c8-aa86-279a700a7f58", "value": 0.8}, {"color": "#FF6347", "from": "1b3a4eb6-a80f-4098-b98e-2ca50ecbdbc6", "title": "Type: extracted\u003cbr\u003eRelation: RELATED_TO", "to": "78b888f4-c0bf-492e-b514-3da1f628797d", "value": 0.8}, {"color": "#FF6347", "from": "ffec4610-96c3-4a0f-a592-573143619a30", "title": "Type: extracted\u003cbr\u003eRelation: RELATED_TO", "to": "c7b69b48-9fea-45de-868d-27f935a7b2b7", "value": 0.8}]);
|
| 295 |
+
|
| 296 |
+
nodeColors = {};
|
| 297 |
+
allNodes = nodes.get({ returnType: "Object" });
|
| 298 |
+
for (nodeId in allNodes) {
|
| 299 |
+
nodeColors[nodeId] = allNodes[nodeId].color;
|
| 300 |
+
}
|
| 301 |
+
allEdges = edges.get({ returnType: "Object" });
|
| 302 |
+
// adding nodes and edges to the graph
|
| 303 |
+
data = {nodes: nodes, edges: edges};
|
| 304 |
+
|
| 305 |
+
var options = {
|
| 306 |
+
"configure": {
|
| 307 |
+
"enabled": true,
|
| 308 |
+
"filter": [
|
| 309 |
+
"physics",
|
| 310 |
+
"nodes",
|
| 311 |
+
"edges"
|
| 312 |
+
]
|
| 313 |
+
},
|
| 314 |
+
"edges": {
|
| 315 |
+
"color": {
|
| 316 |
+
"inherit": true
|
| 317 |
+
},
|
| 318 |
+
"smooth": {
|
| 319 |
+
"enabled": true,
|
| 320 |
+
"type": "dynamic"
|
| 321 |
+
}
|
| 322 |
+
},
|
| 323 |
+
"interaction": {
|
| 324 |
+
"dragNodes": true,
|
| 325 |
+
"hideEdgesOnDrag": false,
|
| 326 |
+
"hideNodesOnDrag": false
|
| 327 |
+
},
|
| 328 |
+
"physics": {
|
| 329 |
+
"barnesHut": {
|
| 330 |
+
"avoidOverlap": 0,
|
| 331 |
+
"centralGravity": 0.1,
|
| 332 |
+
"damping": 0.09,
|
| 333 |
+
"gravitationalConstant": -8000,
|
| 334 |
+
"springConstant": 0.005,
|
| 335 |
+
"springLength": 150
|
| 336 |
+
},
|
| 337 |
+
"enabled": true,
|
| 338 |
+
"stabilization": {
|
| 339 |
+
"enabled": true,
|
| 340 |
+
"fit": true,
|
| 341 |
+
"iterations": 1000,
|
| 342 |
+
"onlyDynamicEdges": false,
|
| 343 |
+
"updateInterval": 50
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
};
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
// if this network requires displaying the configure window,
|
| 353 |
+
// put it in its div
|
| 354 |
+
options.configure["container"] = document.getElementById("config");
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
network = new vis.Network(container, data, options);
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
return network;
|
| 369 |
+
|
| 370 |
+
}
|
| 371 |
+
drawGraph();
|
| 372 |
+
</script>
|
| 373 |
+
</body>
|
| 374 |
+
</html>
|
concept_similarities.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d17d91d0e64c82d91352c7d178c5f0bf6d19719c3c190036fad8395f7652fa72
|
| 3 |
+
size 3421
|
concepts.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c8e21aae40aeb1ca4155d06f5df51e5d63be4aecb619ad8d15f7fbca58e9a7e6
|
| 3 |
+
size 3200
|
documents.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:158b86abf339fb8862e128d62c1e64e4dc4536a3937eaf85e44e839eb12448f4
|
| 3 |
+
size 3921
|
extractor.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/extraction/extractor.py (AttributeError DÜZELTİLMİŞ TAM KOD)
|
| 2 |
+
|
| 3 |
+
import spacy
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import logging
|
| 6 |
+
import itertools
|
| 7 |
+
import re
|
| 8 |
+
import string
|
| 9 |
+
|
| 10 |
+
# Yerel modüllerimizi içe aktaralım
|
| 11 |
+
from src.data_management import storage
|
| 12 |
+
from src.data_management import loaders # extract_text_from_pdf için
|
| 13 |
+
|
| 14 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 15 |
+
|
| 16 |
+
# --- spaCy Model Yükleme ---
|
| 17 |
+
nlp = None
|
| 18 |
+
STOP_WORDS = set()
|
| 19 |
+
try:
|
| 20 |
+
nlp = spacy.load("en_core_web_lg")
|
| 21 |
+
logging.info("spaCy 'en_core_web_lg' modeli başarıyla yüklendi.")
|
| 22 |
+
STOP_WORDS = nlp.Defaults.stop_words
|
| 23 |
+
except OSError:
|
| 24 |
+
logging.error("spaCy 'en_core_web_lg' modeli bulunamadı. Lütfen indirin: python -m spacy download en_core_web_lg")
|
| 25 |
+
|
| 26 |
+
# --- Konsept Belirleme Kriterleri (Aynı kaldı) ---
|
| 27 |
+
TRUSTED_ENTITY_LABELS = {"PRODUCT", "ORG", "WORK_OF_ART"}
|
| 28 |
+
OTHER_ENTITY_LABELS = {"PERSON", "EVENT", "LAW", "NORP", "FAC", "GPE", "LOC"}
|
| 29 |
+
NOUN_CHUNK_PATTERNS = re.compile(r".*\b(learning|network|model|algorithm|system|technique|approach|agent|layer|architecture|transformer|attention)\b$", re.IGNORECASE)
|
| 30 |
+
MIN_CONCEPT_WORDS = 1
|
| 31 |
+
MAX_CONCEPT_WORDS = 6
|
| 32 |
+
AI_KEYWORDS = {"artificial intelligence", "machine learning", "deep learning",
|
| 33 |
+
"neural network", "reinforcement learning", "transformer", "llm",
|
| 34 |
+
"large language model", "computer vision", "natural language processing",
|
| 35 |
+
"algorithm", "model", "gpt", "bert", "agent", "attention", "supervised",
|
| 36 |
+
"unsupervised", "classification", "regression", "clustering"}
|
| 37 |
+
# --- İlişki Çıkarımı için Fiiller ve Desenler ---
|
| 38 |
+
RELATION_VERBS = {
|
| 39 |
+
"use": "USES", "utilize": "USES", "apply": "USES", "employ": "USES",
|
| 40 |
+
"improve": "IMPROVES", "enhance": "IMPROVES", "extend": "IMPROVES", "outperform": "IMPROVES",
|
| 41 |
+
"base on": "BASED_ON", "rely on": "BASED_ON",
|
| 42 |
+
"compare": "COMPARES_TO", "relate": "RELATED_TO", "associate": "RELATED_TO", "link": "RELATED_TO",
|
| 43 |
+
"propose": "PROPOSES", "introduce": "PROPOSES", "develop": "PROPOSES",
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
def normalize_and_validate_concept(text: str, is_entity: bool = False, entity_label: str = "") -> str | None:
|
| 47 |
+
""" Verilen metni temizler, doğrular... """
|
| 48 |
+
cleaned_text = text.strip()
|
| 49 |
+
word_count = len(cleaned_text.split())
|
| 50 |
+
if not (MIN_CONCEPT_WORDS <= word_count <= MAX_CONCEPT_WORDS): return None
|
| 51 |
+
if cleaned_text and all(word.lower() in STOP_WORDS for word in re.findall(r'\b\w+\b', cleaned_text)): return None
|
| 52 |
+
if cleaned_text.isdigit() or all(c in string.punctuation for c in cleaned_text): return None
|
| 53 |
+
generic_phrases = {"this approach", "these models", "this technique", "this system",
|
| 54 |
+
"the model", "the algorithm", "the method", "the approach",
|
| 55 |
+
"the system", "the technique", "our model", "our approach"}
|
| 56 |
+
if cleaned_text.lower() in generic_phrases: return None
|
| 57 |
+
return cleaned_text
|
| 58 |
+
|
| 59 |
+
def find_verb_relation(token1: spacy.tokens.Token, token2: spacy.tokens.Token) -> tuple[str, str] | None:
|
| 60 |
+
""" İki token arasındaki dependency path'e bakarak fiil ilişkisi bulur. """
|
| 61 |
+
common_ancestor = None
|
| 62 |
+
ancestors1 = list(token1.ancestors)
|
| 63 |
+
ancestors2 = list(token2.ancestors)
|
| 64 |
+
for t in reversed(ancestors1):
|
| 65 |
+
if t in ancestors2:
|
| 66 |
+
common_ancestor = t
|
| 67 |
+
break
|
| 68 |
+
if not common_ancestor: return None
|
| 69 |
+
|
| 70 |
+
verb1 = None; head = token1
|
| 71 |
+
while head != common_ancestor:
|
| 72 |
+
if head.pos_ == "VERB": verb1 = head; break
|
| 73 |
+
head = head.head
|
| 74 |
+
verb2 = None; head = token2
|
| 75 |
+
while head != common_ancestor:
|
| 76 |
+
if head.pos_ == "VERB": verb2 = head; break
|
| 77 |
+
head = head.head
|
| 78 |
+
|
| 79 |
+
verb_token = None
|
| 80 |
+
if common_ancestor.pos_ == "VERB": verb_token = common_ancestor
|
| 81 |
+
elif verb1 and verb1 == verb2: verb_token = verb1
|
| 82 |
+
# elif verb1: verb_token = verb1 # Tek taraflı fiilleri şimdilik yoksayalım
|
| 83 |
+
# elif verb2: verb_token = verb2
|
| 84 |
+
elif common_ancestor.head.pos_ == "VERB": verb_token = common_ancestor.head
|
| 85 |
+
|
| 86 |
+
if verb_token:
|
| 87 |
+
verb_lemma = verb_token.lemma_
|
| 88 |
+
# *** HATA DÜZELTME: Bu satırı geçici olarak kaldırıyoruz/yorum yapıyoruz ***
|
| 89 |
+
# if verb_token.is_aux or verb_token.is_stop:
|
| 90 |
+
# return None
|
| 91 |
+
# **********************************************************************
|
| 92 |
+
for verb, rel_type in RELATION_VERBS.items():
|
| 93 |
+
if verb_lemma == verb or verb_lemma in verb.split():
|
| 94 |
+
logging.debug(f"Fiil ilişkisi bulundu: {token1.text}... {verb_lemma} ({rel_type}) ...{token2.text}")
|
| 95 |
+
return rel_type, verb_lemma
|
| 96 |
+
return None
|
| 97 |
+
|
| 98 |
+
def extract_entities_and_relations(text: str, doc_id: str):
|
| 99 |
+
""" Metinden konseptleri, mention'ları ve İYİLEŞTİRİLMİŞ ilişkileri çıkarır. """
|
| 100 |
+
if not nlp: raise RuntimeError("spaCy modeli yüklenemedi.")
|
| 101 |
+
spacy_doc = nlp(text)
|
| 102 |
+
potential_concepts = {}; mentions_in_doc = []; valid_mentions = {}
|
| 103 |
+
processed_spans = set(); added_relations = set()
|
| 104 |
+
|
| 105 |
+
# 1. Adayları Bul
|
| 106 |
+
candidates = []
|
| 107 |
+
for ent in spacy_doc.ents:
|
| 108 |
+
if ent.label_ in TRUSTED_ENTITY_LABELS or ent.label_ in OTHER_ENTITY_LABELS:
|
| 109 |
+
candidates.append({"span": ent, "is_entity": True, "label": ent.label_})
|
| 110 |
+
for chunk in spacy_doc.noun_chunks:
|
| 111 |
+
is_covered = any(ent_data["span"].start_char <= chunk.start_char and ent_data["span"].end_char >= chunk.end_char
|
| 112 |
+
for ent_data in candidates if ent_data["is_entity"])
|
| 113 |
+
if not is_covered:
|
| 114 |
+
candidates.append({"span": chunk, "is_entity": False, "label": ""})
|
| 115 |
+
|
| 116 |
+
# 2. Adayları Filtrele, Normalleştir ve Kaydet
|
| 117 |
+
for data in candidates:
|
| 118 |
+
span = data["span"];
|
| 119 |
+
if span in processed_spans: continue
|
| 120 |
+
validated_text = normalize_and_validate_concept(span.text, data["is_entity"], data["label"])
|
| 121 |
+
if not validated_text: processed_spans.add(span); continue
|
| 122 |
+
concept_lemma = span.lemma_.lower().strip() if span.lemma_ else validated_text.lower()
|
| 123 |
+
is_concept = False
|
| 124 |
+
if data["is_entity"] and data["label"] in TRUSTED_ENTITY_LABELS: is_concept = True
|
| 125 |
+
elif NOUN_CHUNK_PATTERNS.match(validated_text): is_concept = True
|
| 126 |
+
elif any(keyword in concept_lemma.split() or keyword in validated_text.lower().split() for keyword in AI_KEYWORDS): is_concept = True
|
| 127 |
+
elif validated_text.isupper() and len(validated_text) > 1 and len(validated_text) < 6: is_concept = True
|
| 128 |
+
|
| 129 |
+
if is_concept:
|
| 130 |
+
concept_id = storage.add_concept(validated_text)
|
| 131 |
+
if concept_id:
|
| 132 |
+
mention_id = storage.add_mention(
|
| 133 |
+
doc_id=doc_id, concept_id=concept_id,
|
| 134 |
+
context=span.sent.text, start=span.start_char, end=span.end_char
|
| 135 |
+
)
|
| 136 |
+
if mention_id:
|
| 137 |
+
mention_data = {
|
| 138 |
+
"mention_id": mention_id, "concept_id": concept_id,
|
| 139 |
+
"start_char": span.start_char, "end_char": span.end_char,
|
| 140 |
+
"sentence": span.sent, "root_token": span.root
|
| 141 |
+
}
|
| 142 |
+
mentions_in_doc.append(mention_data); valid_mentions[mention_id] = mention_data
|
| 143 |
+
processed_spans.add(span)
|
| 144 |
+
|
| 145 |
+
# 3. İlişkileri Çıkar
|
| 146 |
+
for sentence in spacy_doc.sents:
|
| 147 |
+
mentions_in_sentence = [m for m in mentions_in_doc if m["sentence"] == sentence]
|
| 148 |
+
if len(mentions_in_sentence) >= 2:
|
| 149 |
+
for m1_data, m2_data in itertools.combinations(mentions_in_sentence, 2):
|
| 150 |
+
c1_id = m1_data["concept_id"]; c2_id = m2_data["concept_id"]
|
| 151 |
+
if c1_id == c2_id: continue
|
| 152 |
+
rel_pair = tuple(sorted((c1_id, c2_id)))
|
| 153 |
+
if rel_pair in added_relations: continue
|
| 154 |
+
relation_found = False
|
| 155 |
+
relation_info = find_verb_relation(m1_data["root_token"], m2_data["root_token"])
|
| 156 |
+
if relation_info:
|
| 157 |
+
rel_type, verb = relation_info
|
| 158 |
+
storage.add_relationship(
|
| 159 |
+
source_concept_id=c1_id, target_concept_id=c2_id, rel_type=rel_type,
|
| 160 |
+
mention_id=m1_data["mention_id"], doc_id=doc_id, sentence=sentence.text
|
| 161 |
+
)
|
| 162 |
+
relation_found = True; added_relations.add(rel_pair)
|
| 163 |
+
if not relation_found:
|
| 164 |
+
storage.add_relationship(
|
| 165 |
+
source_concept_id=c1_id, target_concept_id=c2_id, rel_type="RELATED_TO",
|
| 166 |
+
mention_id=m1_data["mention_id"], doc_id=doc_id, sentence=sentence.text
|
| 167 |
+
)
|
| 168 |
+
added_relations.add(rel_pair)
|
| 169 |
+
|
| 170 |
+
def process_documents_for_extraction():
|
| 171 |
+
""" Dokümanları işler ve durumu günceller... (Öncekiyle aynı) """
|
| 172 |
+
if not nlp: raise RuntimeError("spaCy modeli yüklenemedi.")
|
| 173 |
+
logging.info("Gelişmiş bilgi çıkarımı için dokümanlar işleniyor...")
|
| 174 |
+
documents_df = storage.load_dataframe('documents', storage.DOC_COLUMNS)
|
| 175 |
+
docs_to_process = documents_df[documents_df['status'] == 'added']
|
| 176 |
+
if docs_to_process.empty:
|
| 177 |
+
logging.info("Durumu 'added' olan ve işlenecek doküman bulunamadı.")
|
| 178 |
+
return
|
| 179 |
+
processed_count = 0; failed_count = 0
|
| 180 |
+
for index, doc_row in docs_to_process.iterrows():
|
| 181 |
+
doc_id = doc_row['doc_id']; filepath = Path(doc_row['filepath'])
|
| 182 |
+
logging.info(f"İşleniyor: {filepath.name} (ID: {doc_id})")
|
| 183 |
+
text = loaders.extract_text_from_pdf(filepath)
|
| 184 |
+
if text:
|
| 185 |
+
try:
|
| 186 |
+
extract_entities_and_relations(text, doc_id)
|
| 187 |
+
storage.update_document_status(doc_id, 'processed_v3') # Yeni versiyon durumu
|
| 188 |
+
processed_count += 1
|
| 189 |
+
except Exception as e:
|
| 190 |
+
logging.exception(f"'{filepath.name}' işlenirken BEKLENMEYEN HATA oluştu: {e}")
|
| 191 |
+
storage.update_document_status(doc_id, 'extraction_failed_v3')
|
| 192 |
+
failed_count += 1
|
| 193 |
+
else:
|
| 194 |
+
logging.warning(f"Metin çıkarılamadı: {filepath.name}")
|
| 195 |
+
storage.update_document_status(doc_id, 'text_extraction_failed')
|
| 196 |
+
failed_count += 1
|
| 197 |
+
logging.info(f"Gelişmiş bilgi çıkarımı tamamlandı. Başarılı: {processed_count}, Başarısız: {failed_count}")
|
loaders.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import PyPDF2 # PDF dosyalarını okumak için
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import logging
|
| 5 |
+
import re # Tarih ayrıştırma için Regular Expressions
|
| 6 |
+
|
| 7 |
+
# Mevcut modüldeki storage fonksiyonlarını içe aktar (aynı klasörde olduğu için .)
|
| 8 |
+
from .storage import add_document, load_dataframe, save_dataframe, DOC_COLUMNS
|
| 9 |
+
|
| 10 |
+
# Ham veri klasörünün yolu
|
| 11 |
+
RAW_DATA_PATH = Path("data/raw")
|
| 12 |
+
|
| 13 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 14 |
+
|
| 15 |
+
def extract_text_from_pdf(pdf_path: Path) -> str | None:
|
| 16 |
+
"""
|
| 17 |
+
Verilen PDF dosyasının metin içeriğini çıkarır.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
pdf_path (Path): PDF dosyasının yolu.
|
| 21 |
+
|
| 22 |
+
Returns:
|
| 23 |
+
str | None: Çıkarılan metin veya hata durumunda None.
|
| 24 |
+
"""
|
| 25 |
+
try:
|
| 26 |
+
with open(pdf_path, 'rb') as file:
|
| 27 |
+
reader = PyPDF2.PdfReader(file)
|
| 28 |
+
text = ""
|
| 29 |
+
for page in reader.pages:
|
| 30 |
+
page_text = page.extract_text()
|
| 31 |
+
if page_text:
|
| 32 |
+
text += page_text + "\n" # Sayfalar arasına yeni satır ekle
|
| 33 |
+
logging.info(f"Metin çıkarıldı: {pdf_path.name}")
|
| 34 |
+
return text
|
| 35 |
+
except Exception as e:
|
| 36 |
+
logging.error(f"PDF metni çıkarılırken hata ({pdf_path.name}): {e}")
|
| 37 |
+
# Şifreli PDF'ler veya bozuk dosyalar PyPDF2 tarafından hata verebilir
|
| 38 |
+
if "password" in str(e).lower():
|
| 39 |
+
logging.warning(f"Dosya şifreli olabilir: {pdf_path.name}")
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
def parse_date_from_filename(filename: str) -> datetime | None:
|
| 43 |
+
"""
|
| 44 |
+
Dosya adından YYYY-MM-DD veya YYYYMMDD formatında tarih ayrıştırmaya çalışır.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
filename (str): Dosya adı.
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
datetime | None: Bulunan tarih veya None.
|
| 51 |
+
"""
|
| 52 |
+
# Örnek: 2023-10-26_paper.pdf, 20231026-paper.pdf, 2023_10_26 paper.pdf
|
| 53 |
+
patterns = [
|
| 54 |
+
r"(\d{4}-\d{2}-\d{2})", # YYYY-MM-DD
|
| 55 |
+
r"(\d{4}_\d{2}_\d{2})", # YYYY_MM_DD
|
| 56 |
+
r"(\d{8})" # YYYYMMDD
|
| 57 |
+
]
|
| 58 |
+
for pattern in patterns:
|
| 59 |
+
match = re.search(pattern, filename)
|
| 60 |
+
if match:
|
| 61 |
+
date_str = match.group(1).replace("_", "-") # Alt çizgiyi tireye çevir
|
| 62 |
+
try:
|
| 63 |
+
# Sadece tarih kısmını al, saat bilgisi ekleme
|
| 64 |
+
return datetime.strptime(date_str, '%Y-%m-%d').date()
|
| 65 |
+
except ValueError:
|
| 66 |
+
continue # Geçersiz tarih formatı varsa diğer deseni dene
|
| 67 |
+
logging.warning(f"Dosya adından geçerli tarih ayrıştırılamadı: {filename}")
|
| 68 |
+
return None
|
| 69 |
+
|
| 70 |
+
def process_raw_documents():
|
| 71 |
+
"""
|
| 72 |
+
'data/raw/' klasöründeki tüm PDF dosyalarını işler,
|
| 73 |
+
tarihlerini ayrıştırır ve sisteme ekler (eğer zaten ekli değillerse).
|
| 74 |
+
"""
|
| 75 |
+
if not RAW_DATA_PATH.exists():
|
| 76 |
+
logging.error(f"Ham veri klasörü bulunamadı: {RAW_DATA_PATH}")
|
| 77 |
+
return
|
| 78 |
+
|
| 79 |
+
logging.info(f"'{RAW_DATA_PATH}' klasöründeki PDF dosyaları işleniyor...")
|
| 80 |
+
processed_count = 0
|
| 81 |
+
added_count = 0
|
| 82 |
+
|
| 83 |
+
# Tüm PDF dosyalarını bul
|
| 84 |
+
pdf_files = list(RAW_DATA_PATH.glob('*.pdf'))
|
| 85 |
+
|
| 86 |
+
if not pdf_files:
|
| 87 |
+
logging.warning(f"'{RAW_DATA_PATH}' klasöründe işlenecek PDF dosyası bulunamadı.")
|
| 88 |
+
return
|
| 89 |
+
|
| 90 |
+
for pdf_path in pdf_files:
|
| 91 |
+
processed_count += 1
|
| 92 |
+
filename = pdf_path.name
|
| 93 |
+
filepath_str = str(pdf_path.resolve()) # Tam dosya yolunu al
|
| 94 |
+
|
| 95 |
+
# Dosya adından tarihi ayrıştır
|
| 96 |
+
publication_date = parse_date_from_filename(filename)
|
| 97 |
+
|
| 98 |
+
if publication_date:
|
| 99 |
+
# Dokümanı sisteme ekle (storage modülünü kullanarak)
|
| 100 |
+
# add_document, zaten varsa None yerine mevcut ID'yi döndürecek şekilde güncellendi
|
| 101 |
+
doc_id = add_document(filepath_str, publication_date)
|
| 102 |
+
if doc_id:
|
| 103 |
+
# Eğer yeni eklendiyse (veya mevcut ID döndüyse), sayacı artırabiliriz
|
| 104 |
+
# Şimdilik sadece eklenip eklenmediğini kontrol etmek yeterli
|
| 105 |
+
# Gerçek ekleme 'add_document' içinde loglanıyor
|
| 106 |
+
pass # Şimdilik ek bir işlem yapmıyoruz
|
| 107 |
+
|
| 108 |
+
else:
|
| 109 |
+
logging.warning(f"'{filename}' için yayın tarihi bulunamadı, doküman eklenemedi.")
|
| 110 |
+
|
| 111 |
+
logging.info(f"Toplam {processed_count} PDF dosyası tarandı.")
|
| 112 |
+
# Gerçekte kaç tane yeni eklendiği bilgisini storage loglarından takip edebiliriz.
|
| 113 |
+
|
| 114 |
+
# --- Metin Çıkarma ve Kaydetme (Sonraki Fazlar İçin Hazırlık) ---
|
| 115 |
+
# İleride bu fonksiyonu çağırıp metinleri ayrı dosyalara kaydedebiliriz
|
| 116 |
+
# ve documents_df'i güncelleyebiliriz.
|
| 117 |
+
#
|
| 118 |
+
# def extract_and_save_text(doc_id: str, pdf_path: Path):
|
| 119 |
+
# text = extract_text_from_pdf(pdf_path)
|
| 120 |
+
# if text:
|
| 121 |
+
# # Metni kaydet (örn: data/processed_data/text/{doc_id}.txt)
|
| 122 |
+
# text_path = DATA_PATH / "text" / f"{doc_id}.txt"
|
| 123 |
+
# text_path.parent.mkdir(parents=True, exist_ok=True)
|
| 124 |
+
# try:
|
| 125 |
+
# with open(text_path, 'w', encoding='utf-8') as f:
|
| 126 |
+
# f.write(text)
|
| 127 |
+
# logging.info(f"Metin '{text_path}' olarak kaydedildi.")
|
| 128 |
+
# # documents_df'i güncelle (status='text_extracted', processed_text_path=str(text_path))
|
| 129 |
+
# docs_df = load_dataframe('documents', DOC_COLUMNS)
|
| 130 |
+
# doc_index = docs_df[docs_df['doc_id'] == doc_id].index
|
| 131 |
+
# if not doc_index.empty:
|
| 132 |
+
# docs_df.loc[doc_index, 'status'] = 'text_extracted'
|
| 133 |
+
# docs_df.loc[doc_index, 'processed_text_path'] = str(text_path)
|
| 134 |
+
# save_dataframe(docs_df, 'documents')
|
| 135 |
+
# except Exception as e:
|
| 136 |
+
# logging.error(f"Metin kaydedilirken hata ({doc_id}): {e}")
|
mentions.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:49749194f77092f6c3b9e6eacd4ef3a3c34f9d5d1f9c766a51123bdc57885c24
|
| 3 |
+
size 9877
|
network_analysis.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/analysis/network_analysis.py
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
# Topluluk tespiti için Louvain metodu (önce 'pip install python-louvain community' yapılmalı)
|
| 8 |
+
try:
|
| 9 |
+
import community.community_louvain as community_louvain
|
| 10 |
+
community_lib_available = True
|
| 11 |
+
except ImportError:
|
| 12 |
+
logging.warning("'community' (python-louvain) kütüphanesi bulunamadı. Topluluk tespiti yapılamayacak. Kurulum için: pip install python-louvain community")
|
| 13 |
+
community_lib_available = False
|
| 14 |
+
|
| 15 |
+
# Yerel modüller
|
| 16 |
+
from src.data_management import storage
|
| 17 |
+
|
| 18 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 19 |
+
|
| 20 |
+
def calculate_centrality(graph: nx.Graph) -> dict:
|
| 21 |
+
"""
|
| 22 |
+
Graf üzerindeki düğümler için merkeziyet metriklerini hesaplar.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
graph (nx.Graph): Analiz edilecek NetworkX grafı.
|
| 26 |
+
|
| 27 |
+
Returns:
|
| 28 |
+
dict: {node_id: {'degree': float, 'betweenness': float, 'eigenvector': float (veya None)}}
|
| 29 |
+
formatında metrikleri içeren sözlük.
|
| 30 |
+
"""
|
| 31 |
+
metrics = {}
|
| 32 |
+
if not graph or graph.number_of_nodes() == 0:
|
| 33 |
+
return metrics
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
degree_centrality = nx.degree_centrality(graph)
|
| 37 |
+
except Exception as e:
|
| 38 |
+
logging.error(f"Degree Centrality hesaplanırken hata: {e}")
|
| 39 |
+
degree_centrality = {}
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
betweenness_centrality = nx.betweenness_centrality(graph)
|
| 43 |
+
except Exception as e:
|
| 44 |
+
logging.error(f"Betweenness Centrality hesaplanırken hata: {e}")
|
| 45 |
+
betweenness_centrality = {}
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
# Eigenvector centrality bağlantısız (disconnected) graflarda veya bazı durumlarda hata verebilir
|
| 49 |
+
# max_iter artırılabilir veya hata yakalanabilir
|
| 50 |
+
eigenvector_centrality = nx.eigenvector_centrality(graph, max_iter=500, tol=1e-06)
|
| 51 |
+
except Exception as e:
|
| 52 |
+
logging.warning(f"Eigenvector Centrality hesaplanırken hata (graf bağlantısız olabilir): {e}")
|
| 53 |
+
eigenvector_centrality = {} # Hata durumunda boş bırak
|
| 54 |
+
|
| 55 |
+
# Metrikleri birleştir
|
| 56 |
+
for node in graph.nodes():
|
| 57 |
+
metrics[node] = {
|
| 58 |
+
'degree_centrality': degree_centrality.get(node, 0.0),
|
| 59 |
+
'betweenness_centrality': betweenness_centrality.get(node, 0.0),
|
| 60 |
+
'eigenvector_centrality': eigenvector_centrality.get(node, None) # Hata durumunda None olabilir
|
| 61 |
+
}
|
| 62 |
+
logging.info("Merkeziyet metrikleri hesaplandı.")
|
| 63 |
+
return metrics
|
| 64 |
+
|
| 65 |
+
def detect_communities(graph: nx.Graph) -> dict | None:
|
| 66 |
+
"""
|
| 67 |
+
Louvain algoritması kullanarak graf üzerindeki toplulukları tespit eder.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
graph (nx.Graph): Analiz edilecek NetworkX grafı.
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
dict | None: {node_id: community_id} formatında bölümleme sözlüğü veya hata/kütüphane yoksa None.
|
| 74 |
+
"""
|
| 75 |
+
if not community_lib_available:
|
| 76 |
+
return None # Kütüphane yoksa hesaplama yapma
|
| 77 |
+
if not graph or graph.number_of_nodes() == 0:
|
| 78 |
+
return None # Boş graf
|
| 79 |
+
|
| 80 |
+
# Louvain metodu yönlendirilmemiş graflarda daha iyi çalışır.
|
| 81 |
+
# Eğer graf yönlü ise, yönlendirilmemişe çevir (veya uyarı ver).
|
| 82 |
+
# Bizim grafımız zaten yönlendirilmemiş (nx.Graph).
|
| 83 |
+
# Ağırlıklı kenarları kullanabilir (varsayılan weight='weight')
|
| 84 |
+
try:
|
| 85 |
+
partition = community_louvain.best_partition(graph, weight='weight') # Kenar ağırlıklarını dikkate al
|
| 86 |
+
num_communities = len(set(partition.values()))
|
| 87 |
+
logging.info(f"Louvain ile topluluk tespiti tamamlandı. {num_communities} topluluk bulundu.")
|
| 88 |
+
return partition
|
| 89 |
+
except Exception as e:
|
| 90 |
+
logging.exception(f"Topluluk tespiti sırasında hata oluştu: {e}")
|
| 91 |
+
return None
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def get_network_analysis_results(graph: nx.Graph) -> pd.DataFrame | None:
|
| 95 |
+
"""
|
| 96 |
+
Merkeziyet ve topluluk analizlerini yapar ve sonuçları bir DataFrame'de birleştirir.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
graph (nx.Graph): Analiz edilecek NetworkX grafı.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
pd.DataFrame | None: 'concept_id', 'name', 'degree_centrality', 'betweenness_centrality',
|
| 103 |
+
'eigenvector_centrality', 'community_id' sütunlarını içeren DataFrame
|
| 104 |
+
veya hata durumunda None.
|
| 105 |
+
"""
|
| 106 |
+
if not graph or graph.number_of_nodes() == 0:
|
| 107 |
+
logging.warning("Analiz için boş veya geçersiz graf sağlandı.")
|
| 108 |
+
return None
|
| 109 |
+
|
| 110 |
+
logging.info("Ağ analizi metrikleri hesaplanıyor...")
|
| 111 |
+
centrality_metrics = calculate_centrality(graph)
|
| 112 |
+
community_partition = detect_communities(graph)
|
| 113 |
+
|
| 114 |
+
# Sonuçları bir DataFrame'e dönüştür
|
| 115 |
+
analysis_data = []
|
| 116 |
+
concepts_df = storage.load_dataframe('concepts', storage.CONCEPT_COLUMNS) # İsimler için yükle
|
| 117 |
+
|
| 118 |
+
for node_id, metrics in centrality_metrics.items():
|
| 119 |
+
node_data = {
|
| 120 |
+
'concept_id': node_id,
|
| 121 |
+
'name': graph.nodes[node_id].get('name', 'N/A'), # Graf düğümünden al
|
| 122 |
+
'degree_centrality': metrics.get('degree_centrality'),
|
| 123 |
+
'betweenness_centrality': metrics.get('betweenness_centrality'),
|
| 124 |
+
'eigenvector_centrality': metrics.get('eigenvector_centrality'),
|
| 125 |
+
'community_id': community_partition.get(node_id, -1) if community_partition else -1 # Topluluk yoksa -1
|
| 126 |
+
}
|
| 127 |
+
analysis_data.append(node_data)
|
| 128 |
+
|
| 129 |
+
if not analysis_data:
|
| 130 |
+
logging.warning("Ağ analizi sonucu veri üretilemedi.")
|
| 131 |
+
return None
|
| 132 |
+
|
| 133 |
+
analysis_df = pd.DataFrame(analysis_data)
|
| 134 |
+
|
| 135 |
+
# Eğer graf düğümlerinde isim yoksa, concepts_df'ten almayı dene (yedek)
|
| 136 |
+
if 'N/A' in analysis_df['name'].values and concepts_df is not None:
|
| 137 |
+
analysis_df = analysis_df.drop(columns=['name']) # Eski 'name' sütununu sil
|
| 138 |
+
analysis_df = pd.merge(analysis_df, concepts_df[['concept_id', 'name']], on='concept_id', how='left')
|
| 139 |
+
# Sütun sırasını ayarla
|
| 140 |
+
cols = ['concept_id', 'name'] + [col for col in analysis_df.columns if col not in ['concept_id', 'name']]
|
| 141 |
+
analysis_df = analysis_df[cols]
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
logging.info("Ağ analizi sonuçları DataFrame'e dönüştürüldü.")
|
| 145 |
+
return analysis_df
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def save_network_analysis(analysis_df: pd.DataFrame):
|
| 149 |
+
""" Ağ analizi sonuçlarını Parquet dosyasına kaydeder. """
|
| 150 |
+
if analysis_df is not None and not analysis_df.empty:
|
| 151 |
+
storage.save_dataframe(analysis_df, storage.NETWORK_ANALYSIS_FILENAME)
|
| 152 |
+
logging.info(f"Ağ analizi sonuçları '{storage.NETWORK_ANALYSIS_FILENAME}.parquet' olarak kaydedildi.")
|
| 153 |
+
else:
|
| 154 |
+
logging.warning("Kaydedilecek ağ analizi sonucu bulunamadı.")
|
network_builder.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/analysis/network_builder.py (DÜZELTİLMİŞ TAM KOD)
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
# Yerel modüller
|
| 8 |
+
from src.data_management import storage
|
| 9 |
+
|
| 10 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 11 |
+
|
| 12 |
+
# Grafı kaydetmek için dosya adı
|
| 13 |
+
GRAPH_FILENAME = "concept_network"
|
| 14 |
+
# Benzerlik dosyasının adı (Doğrudan burada tanımlayalım veya similarity'den import edelim)
|
| 15 |
+
# storage modülünde değil!
|
| 16 |
+
SIMILARITY_FILENAME = "concept_similarities"
|
| 17 |
+
|
| 18 |
+
def build_concept_network(similarity_threshold: float = 0.60,
|
| 19 |
+
include_similarity_edges: bool = True,
|
| 20 |
+
include_extracted_edges: bool = True) -> nx.Graph | None:
|
| 21 |
+
"""
|
| 22 |
+
Konseptler, çıkarılmış ilişkiler ve anlamsal benzerliklerden bir NetworkX grafı oluşturur.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
similarity_threshold (float): Grafiğe eklenecek minimum anlamsal benzerlik skoru.
|
| 26 |
+
include_similarity_edges (bool): Benzerlik kenarlarını dahil et.
|
| 27 |
+
include_extracted_edges (bool): Metinden çıkarılan ilişki kenarlarını dahil et.
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
nx.Graph | None: Oluşturulan NetworkX grafı veya hata durumunda None.
|
| 31 |
+
"""
|
| 32 |
+
logging.info("Konsept ağı oluşturuluyor...")
|
| 33 |
+
if not include_similarity_edges and not include_extracted_edges:
|
| 34 |
+
logging.warning("Hem benzerlik hem de çıkarılmış ilişki kenarları devre dışı bırakıldı.")
|
| 35 |
+
|
| 36 |
+
# Temel verileri yükle
|
| 37 |
+
concepts_df = storage.load_dataframe('concepts', storage.CONCEPT_COLUMNS)
|
| 38 |
+
relationships_df = storage.load_dataframe('relationships', storage.RELATIONSHIP_COLUMNS)
|
| 39 |
+
# *** DÜZELTME: SIMILARITY_FILENAME doğrudan kullanılıyor ***
|
| 40 |
+
similarity_df = storage.load_dataframe(SIMILARITY_FILENAME, ['concept_id_1', 'concept_id_2', 'similarity'])
|
| 41 |
+
|
| 42 |
+
if concepts_df is None or concepts_df.empty:
|
| 43 |
+
logging.error("Ağ oluşturmak için konsept verisi bulunamadı.")
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
G = nx.Graph()
|
| 47 |
+
|
| 48 |
+
# 1. Adım: Konseptleri Düğüm Olarak Ekle
|
| 49 |
+
node_count = 0
|
| 50 |
+
valid_concept_ids = set() # Grafiğe eklenen geçerli ID'leri takip et
|
| 51 |
+
for index, row in concepts_df.iterrows():
|
| 52 |
+
concept_id = row['concept_id']
|
| 53 |
+
concept_name = row['name']
|
| 54 |
+
if pd.notna(concept_id) and pd.notna(concept_name):
|
| 55 |
+
G.add_node(concept_id, name=concept_name)
|
| 56 |
+
valid_concept_ids.add(concept_id)
|
| 57 |
+
node_count += 1
|
| 58 |
+
else:
|
| 59 |
+
logging.warning(f"Geçersiz konsept verisi atlandı: ID={concept_id}, Name={concept_name}")
|
| 60 |
+
logging.info(f"{node_count} konsept düğüm olarak eklendi.")
|
| 61 |
+
|
| 62 |
+
edge_count_extracted = 0
|
| 63 |
+
edge_count_similarity = 0
|
| 64 |
+
updated_edge_count = 0
|
| 65 |
+
|
| 66 |
+
# 2. Adım: Çıkarılmış İlişkileri Kenar Olarak Ekle
|
| 67 |
+
if include_extracted_edges and relationships_df is not None and not relationships_df.empty:
|
| 68 |
+
logging.info("Çıkarılmış ilişkiler kenar olarak ekleniyor...")
|
| 69 |
+
for index, row in relationships_df.iterrows():
|
| 70 |
+
source_id = row['source_concept_id']
|
| 71 |
+
target_id = row['target_concept_id']
|
| 72 |
+
rel_type = row['type'] or 'RELATED_TO'
|
| 73 |
+
|
| 74 |
+
# Düğümlerin grafide olduğundan ve geçerli olduğundan emin ol
|
| 75 |
+
if source_id in valid_concept_ids and target_id in valid_concept_ids:
|
| 76 |
+
if G.has_edge(source_id, target_id):
|
| 77 |
+
G.edges[source_id, target_id]['relation_type'] = rel_type
|
| 78 |
+
G.edges[source_id, target_id]['type'] = 'extracted'
|
| 79 |
+
else:
|
| 80 |
+
G.add_edge(source_id, target_id, type='extracted', relation_type=rel_type, weight=0.8)
|
| 81 |
+
edge_count_extracted += 1
|
| 82 |
+
else:
|
| 83 |
+
logging.warning(f"İlişki için düğüm(ler) bulunamadı veya geçersiz: {source_id} -> {target_id}")
|
| 84 |
+
logging.info(f"{edge_count_extracted} çıkarılmış ilişki kenarı eklendi.")
|
| 85 |
+
|
| 86 |
+
# 3. Adım: Anlamsal Benzerlikleri Kenar Olarak Ekle
|
| 87 |
+
if include_similarity_edges and similarity_df is not None and not similarity_df.empty:
|
| 88 |
+
logging.info(f"Anlamsal benzerlikler (Eşik > {similarity_threshold:.2f}) kenar olarak ekleniyor...")
|
| 89 |
+
filtered_similarity = similarity_df[(similarity_df['similarity'] >= similarity_threshold) & (similarity_df['similarity'] < 1.0)]
|
| 90 |
+
logging.info(f"{len(similarity_df)} benzerlik çiftinden {len(filtered_similarity)} tanesi eşik değerinin üzerinde (ve < 1.0).")
|
| 91 |
+
|
| 92 |
+
for index, row in filtered_similarity.iterrows():
|
| 93 |
+
id1 = row['concept_id_1']
|
| 94 |
+
id2 = row['concept_id_2']
|
| 95 |
+
similarity = row['similarity']
|
| 96 |
+
|
| 97 |
+
if id1 in valid_concept_ids and id2 in valid_concept_ids:
|
| 98 |
+
if G.has_edge(id1, id2):
|
| 99 |
+
G.edges[id1, id2]['similarity'] = similarity
|
| 100 |
+
if 'weight' not in G.edges[id1, id2] or similarity > G.edges[id1, id2].get('weight', 0):
|
| 101 |
+
G.edges[id1, id2]['weight'] = similarity
|
| 102 |
+
# Eğer extracted ilişki varsa, tipi 'combined' yapabiliriz?
|
| 103 |
+
G.edges[id1, id2]['type'] = 'combined' if G.edges[id1, id2].get('type') == 'extracted' else G.edges[id1, id2].get('type', 'similarity') # Önceliği koru veya birleştir
|
| 104 |
+
updated_edge_count += 1
|
| 105 |
+
else:
|
| 106 |
+
G.add_edge(id1, id2, type='similarity', weight=similarity)
|
| 107 |
+
edge_count_similarity += 1
|
| 108 |
+
else:
|
| 109 |
+
logging.warning(f"Benzerlik için düğüm(ler) bulunamadı veya geçersiz: {id1} <-> {id2}")
|
| 110 |
+
logging.info(f"{edge_count_similarity} yeni benzerlik kenarı eklendi, {updated_edge_count} mevcut kenara benzerlik/tip bilgisi eklendi.")
|
| 111 |
+
|
| 112 |
+
total_edges = G.number_of_edges()
|
| 113 |
+
logging.info(f"Konsept ağı oluşturuldu. Düğüm sayısı: {G.number_of_nodes()}, Kenar sayısı: {total_edges}.")
|
| 114 |
+
|
| 115 |
+
# 4. Adım: Grafı Kaydet
|
| 116 |
+
storage.save_network(G, GRAPH_FILENAME)
|
| 117 |
+
|
| 118 |
+
return G
|
plotting.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/visualization/plotting.py (Ağ Metrikleri ile Görselleştirme Güncellendi)
|
| 2 |
+
|
| 3 |
+
import networkx as nx
|
| 4 |
+
from pyvis.network import Network
|
| 5 |
+
import logging
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import random # Renk paleti için
|
| 9 |
+
|
| 10 |
+
# Yerel modüller
|
| 11 |
+
from src.data_management import storage
|
| 12 |
+
|
| 13 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 14 |
+
|
| 15 |
+
# Görselleştirme dosyalarının kaydedileceği yer
|
| 16 |
+
OUTPUT_DIR = Path("output/graphs")
|
| 17 |
+
DEFAULT_GRAPH_FILENAME = "concept_network"
|
| 18 |
+
# Analiz sonuçları dosyasının adı (storage'dan da alınabilirdi)
|
| 19 |
+
DEFAULT_ANALYSIS_FILENAME = storage.NETWORK_ANALYSIS_FILENAME
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# Basit bir renk paleti (daha fazla renk eklenebilir veya matplotlib colormap kullanılabilir)
|
| 23 |
+
# Viridis, tab10, Set3 gibi paletler iyi çalışır
|
| 24 |
+
# Örnek: import matplotlib.cm as cm; colors = [cm.tab10(i) for i in range(10)]
|
| 25 |
+
DEFAULT_COLORS = [
|
| 26 |
+
"#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
|
| 27 |
+
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
def get_color_for_community(community_id, colors=DEFAULT_COLORS):
|
| 31 |
+
""" Verilen community ID için paletten bir renk döndürür. """
|
| 32 |
+
if community_id < 0 or community_id is None or pd.isna(community_id): # Topluluk yoksa veya geçersizse
|
| 33 |
+
return "#CCCCCC" # Gri
|
| 34 |
+
return colors[int(community_id) % len(colors)] # Modulo ile renk tekrarı
|
| 35 |
+
|
| 36 |
+
def scale_value(value, min_val=0, max_val=1, new_min=10, new_max=50):
|
| 37 |
+
""" Bir değeri belirli bir aralığa ölçekler (örn: merkeziyet -> düğüm boyutu). """
|
| 38 |
+
if max_val == min_val or value is None or pd.isna(value): # Bölme hatasını veya None değerini engelle
|
| 39 |
+
return new_min # Veya ortalama bir değer?
|
| 40 |
+
# Ölçekleme: (value - min) / (max - min) * (new_max - new_min) + new_min
|
| 41 |
+
scaled = ((value - min_val) / (max_val - min_val)) * (new_max - new_min) + new_min
|
| 42 |
+
return max(new_min, min(scaled, new_max)) # Sonuçların min/max arasında kalmasını sağla
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def visualize_network(graph: nx.Graph | None = None,
|
| 46 |
+
graph_filename: str = DEFAULT_GRAPH_FILENAME,
|
| 47 |
+
analysis_filename: str = DEFAULT_ANALYSIS_FILENAME,
|
| 48 |
+
output_filename: str = "concept_network_visualization.html",
|
| 49 |
+
show_buttons: bool = True,
|
| 50 |
+
physics_solver: str = 'barnesHut',
|
| 51 |
+
size_metric: str = 'degree_centrality', # Boyut için kullanılacak metrik
|
| 52 |
+
color_metric: str = 'community_id', # Renk için kullanılacak metrik
|
| 53 |
+
height: str = "800px",
|
| 54 |
+
width: str = "100%"
|
| 55 |
+
) -> str | None:
|
| 56 |
+
"""
|
| 57 |
+
Ağ grafını Pyvis ile görselleştirir. Düğüm boyutu ve rengi için ağ
|
| 58 |
+
analizi metriklerini kullanır.
|
| 59 |
+
"""
|
| 60 |
+
if graph is None:
|
| 61 |
+
logging.info(f"Graf sağlanmadı, '{graph_filename}.pkl' dosyasından yükleniyor...")
|
| 62 |
+
graph = storage.load_network(graph_filename)
|
| 63 |
+
|
| 64 |
+
if graph is None or not isinstance(graph, nx.Graph) or graph.number_of_nodes() == 0:
|
| 65 |
+
logging.error("Görselleştirilecek geçerli veya boş olmayan bir graf bulunamadı.")
|
| 66 |
+
return None
|
| 67 |
+
|
| 68 |
+
# Ağ analizi sonuçlarını yükle
|
| 69 |
+
logging.info(f"Ağ analizi sonuçları '{analysis_filename}.parquet' dosyasından yükleniyor...")
|
| 70 |
+
analysis_df = storage.load_dataframe(analysis_filename, []) # Sütunları bilmediğimiz için boş liste
|
| 71 |
+
metrics_dict = {}
|
| 72 |
+
min_size_val, max_size_val = 0, 1 # Boyut ölçekleme için min/max
|
| 73 |
+
|
| 74 |
+
if analysis_df is not None and not analysis_df.empty and 'concept_id' in analysis_df.columns:
|
| 75 |
+
# Eksik metrik sütunlarını kontrol et ve ekle (NaN ile)
|
| 76 |
+
required_metrics = [size_metric, color_metric]
|
| 77 |
+
for metric in required_metrics:
|
| 78 |
+
if metric not in analysis_df.columns:
|
| 79 |
+
logging.warning(f"Analiz sonuçlarında '{metric}' sütunu bulunamadı. Varsayılan değerler kullanılacak.")
|
| 80 |
+
analysis_df[metric] = None
|
| 81 |
+
|
| 82 |
+
# Boyut metriği için min/max değerleri bul (NaN olmayanlardan)
|
| 83 |
+
if size_metric in analysis_df.columns and analysis_df[size_metric].notna().any():
|
| 84 |
+
min_size_val = analysis_df[size_metric].min()
|
| 85 |
+
max_size_val = analysis_df[size_metric].max()
|
| 86 |
+
|
| 87 |
+
# Kolay erişim için sözlüğe çevir
|
| 88 |
+
metrics_dict = analysis_df.set_index('concept_id').to_dict('index')
|
| 89 |
+
logging.info("Ağ analizi metrikleri yüklendi.")
|
| 90 |
+
else:
|
| 91 |
+
logging.warning("Ağ analizi sonuçları yüklenemedi veya boş. Varsayılan düğüm boyutları/renkleri kullanılacak.")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
logging.info(f"'{output_filename}' için Pyvis ağı oluşturuluyor...")
|
| 95 |
+
net = Network(notebook=False, height=height, width=width, heading='ChronoSense Konsept Ağı (Metriklerle)', cdn_resources='remote')
|
| 96 |
+
net.barnes_hut(gravity=-8000, central_gravity=0.1, spring_length=150, spring_strength=0.005, damping=0.09)
|
| 97 |
+
|
| 98 |
+
# Düğümleri (Nodes) Pyvis'e ekle (Boyut ve Renk ile)
|
| 99 |
+
for node, attrs in graph.nodes(data=True):
|
| 100 |
+
node_label = attrs.get('name', str(node))
|
| 101 |
+
node_metrics = metrics_dict.get(node, {}) # Bu düğüm için metrikleri al, yoksa boş dict
|
| 102 |
+
|
| 103 |
+
# Boyutu hesapla
|
| 104 |
+
size_val = node_metrics.get(size_metric)
|
| 105 |
+
node_size = scale_value(size_val, min_size_val, max_size_val, new_min=10, new_max=40) # 10-40 arası boyut
|
| 106 |
+
|
| 107 |
+
# Rengi hesapla
|
| 108 |
+
color_val = node_metrics.get(color_metric)
|
| 109 |
+
node_color = get_color_for_community(color_val)
|
| 110 |
+
|
| 111 |
+
# Başlığı (Title) güncelle (metrikleri ekle)
|
| 112 |
+
node_title = f"ID: {node}<br>Name: {attrs.get('name', 'N/A')}"
|
| 113 |
+
node_title += f"<br>{size_metric}: {size_val:.3f}" if pd.notna(size_val) else ""
|
| 114 |
+
node_title += f"<br>{color_metric}: {int(color_val)}" if pd.notna(color_val) else ""
|
| 115 |
+
|
| 116 |
+
net.add_node(node, label=node_label, title=node_title, size=node_size, color=node_color)
|
| 117 |
+
|
| 118 |
+
# Kenarları (Edges) Pyvis'e ekle (Öncekiyle aynı, sadece renk/kalınlık ayarları biraz daha belirgin)
|
| 119 |
+
for source, target, attrs in graph.edges(data=True):
|
| 120 |
+
edge_title = f"Type: {attrs.get('type', 'N/A')}"
|
| 121 |
+
edge_value = 0.5 ; edge_color = "#DDDDDD" # Daha soluk varsayılan
|
| 122 |
+
|
| 123 |
+
edge_type = attrs.get('type')
|
| 124 |
+
weight = attrs.get('weight', 0)
|
| 125 |
+
|
| 126 |
+
if edge_type == 'extracted':
|
| 127 |
+
edge_title += f"<br>Relation: {attrs.get('relation_type', 'N/A')}"
|
| 128 |
+
edge_value = max(0.6, weight) # extracted ilişkiler biraz daha belirgin olsun
|
| 129 |
+
edge_color = "#FF6347" # Koyu turuncu/kırmızımsı
|
| 130 |
+
elif edge_type == 'similarity':
|
| 131 |
+
sim_score = attrs.get('similarity', weight)
|
| 132 |
+
edge_title += f"<br>Similarity: {sim_score:.3f}"
|
| 133 |
+
edge_value = sim_score # Benzerlikle orantılı
|
| 134 |
+
edge_color = "#4682B4" # Çelik mavisi
|
| 135 |
+
elif edge_type == 'combined':
|
| 136 |
+
edge_title += f"<br>Relation: {attrs.get('relation_type', 'N/A')}"
|
| 137 |
+
sim_score = attrs.get('similarity', weight)
|
| 138 |
+
edge_title += f"<br>Similarity: {sim_score:.3f}"
|
| 139 |
+
edge_value = max(0.6, sim_score) # Combined da belirgin olsun
|
| 140 |
+
edge_color = "#9370DB" # Orta mor
|
| 141 |
+
|
| 142 |
+
net.add_edge(source, target, title=edge_title, value=max(0.1, edge_value), color=edge_color)
|
| 143 |
+
|
| 144 |
+
if show_buttons:
|
| 145 |
+
net.show_buttons(filter_=['physics', 'nodes', 'edges'])
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 149 |
+
output_path = OUTPUT_DIR / output_filename
|
| 150 |
+
net.save_graph(str(output_path))
|
| 151 |
+
logging.info(f"Ağ görselleştirmesi başarıyla '{output_path}' olarak kaydedildi.")
|
| 152 |
+
return str(output_path)
|
| 153 |
+
except Exception as e:
|
| 154 |
+
logging.exception(f"Ağ görselleştirmesi kaydedilirken hata oluştu: {e}")
|
| 155 |
+
return None
|
relationships.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a41341ec01d3c46cd036ac155cc8ea50d773221f34ab578f067bc21d7581f5fe
|
| 3 |
+
size 10289
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pip install pandas numpy spacy scikit-learn networkx matplotlib plotly pyvis streamlit PyPDF2 sentence-transformers pytest pyarrow
|
| 2 |
+
python -m spacy download en_core_web_lg
|
reset_status.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# reset_status.py
|
| 2 |
+
import pandas as pd
|
| 3 |
+
# storage modülünü doğru import etmek için src'yi sys.path'e ekleyebilir veya PYTHONPATH ayarlayabiliriz.
|
| 4 |
+
# En kolayı çalıştırmadan önce PYTHONPATH ayarlamak veya geçici olarak sys.path'e eklemek.
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 8 |
+
|
| 9 |
+
from src.data_management.storage import load_dataframe, save_dataframe, DOC_COLUMNS
|
| 10 |
+
|
| 11 |
+
print("Doküman durumları 'added' olarak sıfırlanıyor...")
|
| 12 |
+
df = load_dataframe('documents', DOC_COLUMNS)
|
| 13 |
+
|
| 14 |
+
if not df.empty:
|
| 15 |
+
# Sadece işlenmiş veya hata almış olanları sıfırla
|
| 16 |
+
reset_mask = df['status'].str.startswith('processed', na=False) | df['status'].str.contains('failed', na=False)
|
| 17 |
+
if reset_mask.any():
|
| 18 |
+
df.loc[reset_mask, 'status'] = 'added'
|
| 19 |
+
save_dataframe(df, 'documents')
|
| 20 |
+
print(f"{reset_mask.sum()} dokümanın durumu 'added' olarak sıfırlandı.")
|
| 21 |
+
else:
|
| 22 |
+
print("Durumu sıfırlanacak doküman bulunamadı ('processed' veya 'failed' durumunda olan).")
|
| 23 |
+
else:
|
| 24 |
+
print("Doküman DataFrame'i bulunamadı veya boş.")
|
run_analysis.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# run_analysis.py (Ağ Analizi Metrikleri Eklendi)
|
| 2 |
+
import time
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import networkx as nx
|
| 7 |
+
import webbrowser
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
# src klasöründeki modüllere erişim için
|
| 11 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 12 |
+
|
| 13 |
+
from src.analysis.temporal import calculate_concept_frequencies
|
| 14 |
+
from src.analysis.similarity import calculate_concept_embeddings, calculate_similarity_matrix
|
| 15 |
+
from src.analysis.network_builder import build_concept_network
|
| 16 |
+
# YENİ importlar:
|
| 17 |
+
from src.analysis.network_analysis import get_network_analysis_results, save_network_analysis
|
| 18 |
+
from src.visualization.plotting import visualize_network
|
| 19 |
+
from src.data_management.storage import load_dataframe, save_dataframe, CONCEPT_COLUMNS, FREQUENCY_FILENAME, SIMILARITY_FILENAME, NETWORK_ANALYSIS_FILENAME # YENİ: NETWORK_ANALYSIS_FILENAME
|
| 20 |
+
|
| 21 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 22 |
+
pd.set_option('display.max_rows', 100)
|
| 23 |
+
pd.set_option('display.max_columns', 10)
|
| 24 |
+
pd.set_option('display.width', 1000)
|
| 25 |
+
|
| 26 |
+
if __name__ == "__main__":
|
| 27 |
+
print(">>> Analizler Çalıştırılıyor (Frekans + Benzerlik + Ağ + Metrikler + Görselleştirme) <<<")
|
| 28 |
+
overall_start_time = time.time()
|
| 29 |
+
concepts_df = None
|
| 30 |
+
frequency_results_df = None
|
| 31 |
+
similarity_results_df = None
|
| 32 |
+
concept_network = None # Grafı saklamak için
|
| 33 |
+
network_analysis_df = None # Analiz sonuçlarını saklamak için
|
| 34 |
+
|
| 35 |
+
# --- 1. Frekans Analizi ---
|
| 36 |
+
print("\n--- 1. Frekans Hesaplaması ---"); start_time = time.time()
|
| 37 |
+
# ... (önceki kodla aynı, sadece print süresi değişebilir) ...
|
| 38 |
+
frequency_df = calculate_concept_frequencies(time_period='YS')
|
| 39 |
+
if frequency_df is not None:
|
| 40 |
+
concepts_df = load_dataframe('concepts', CONCEPT_COLUMNS)
|
| 41 |
+
if not frequency_df.empty:
|
| 42 |
+
print(f"Toplam {len(frequency_df)} frekans kaydı hesaplandı.")
|
| 43 |
+
if concepts_df is not None and not concepts_df.empty:
|
| 44 |
+
frequency_results_df = pd.merge(frequency_df, concepts_df[['concept_id', 'name']], on='concept_id', how='left')
|
| 45 |
+
frequency_results_df = frequency_results_df[['concept_id', 'name', 'time_period_start', 'frequency']]
|
| 46 |
+
frequency_results_df.sort_values(by=['name', 'time_period_start'], inplace=True)
|
| 47 |
+
print("\n--- Konsept Frekansları (Yıllık) ---"); print(frequency_results_df.to_string())
|
| 48 |
+
save_dataframe(frequency_results_df, FREQUENCY_FILENAME)
|
| 49 |
+
else: print("\nKonsept isimleri yüklenemedi..."); print(frequency_df.to_string())
|
| 50 |
+
else: print("Frekans hesaplandı ancak sonuç boş."); save_dataframe(pd.DataFrame(columns=['concept_id', 'name', 'time_period_start', 'frequency']), FREQUENCY_FILENAME)
|
| 51 |
+
else: print("Frekans hesaplaması sırasında bir hata oluştu.")
|
| 52 |
+
print(f"--- Frekans Hesaplaması Tamamlandı. Süre: {time.time() - start_time:.2f} saniye ---")
|
| 53 |
+
|
| 54 |
+
# --- 2. Anlamsal Benzerlik Analizi ---
|
| 55 |
+
print("\n--- 2. Anlamsal Benzerlik Hesaplaması ---"); start_time = time.time()
|
| 56 |
+
# ... (önceki kodla aynı, sadece print süresi değişebilir) ...
|
| 57 |
+
try:
|
| 58 |
+
concept_embeddings = calculate_concept_embeddings(force_recalculate=False)
|
| 59 |
+
if concept_embeddings:
|
| 60 |
+
similarity_df = calculate_similarity_matrix(concept_embeddings, force_recalculate=False)
|
| 61 |
+
if similarity_df is not None and not similarity_df.empty:
|
| 62 |
+
print(f"Toplam {len(similarity_df)} konsept çifti için benzerlik hesaplandı/yüklendi.")
|
| 63 |
+
if concepts_df is None or concepts_df.empty: concepts_df = load_dataframe('concepts', CONCEPT_COLUMNS)
|
| 64 |
+
if concepts_df is not None and not concepts_df.empty:
|
| 65 |
+
sim_results = pd.merge(similarity_df, concepts_df[['concept_id', 'name']], left_on='concept_id_1', right_on='concept_id', how='left').rename(columns={'name': 'name_1'}).drop(columns=['concept_id'])
|
| 66 |
+
sim_results = pd.merge(sim_results, concepts_df[['concept_id', 'name']], left_on='concept_id_2', right_on='concept_id', how='left').rename(columns={'name': 'name_2'}).drop(columns=['concept_id'])
|
| 67 |
+
sim_results = sim_results[['concept_id_1', 'name_1', 'concept_id_2', 'name_2', 'similarity']]
|
| 68 |
+
sim_results.sort_values(by='similarity', ascending=False, inplace=True)
|
| 69 |
+
similarity_results_df = sim_results
|
| 70 |
+
print("\n--- En Benzer Konsept Çiftleri (Top 20) ---"); print(similarity_results_df.head(20).to_string(index=False))
|
| 71 |
+
save_dataframe(similarity_results_df, SIMILARITY_FILENAME)
|
| 72 |
+
else: print("\nKonsept isimleri yüklenemedi..."); print(similarity_df.sort_values(by='similarity', ascending=False).head(20).to_string(index=False))
|
| 73 |
+
elif similarity_df is not None: print("Benzerlik hesaplandı ancak sonuç boş."); save_dataframe(pd.DataFrame(columns=['concept_id_1', 'name_1', 'concept_id_2', 'name_2', 'similarity']), SIMILARITY_FILENAME)
|
| 74 |
+
except Exception as e: logging.exception("Benzerlik hesaplama sırasında beklenmedik hata oluştu.")
|
| 75 |
+
print(f"--- Benzerlik Hesaplaması Tamamlandı. Süre: {time.time() - start_time:.2f} saniye ---")
|
| 76 |
+
|
| 77 |
+
# --- 3. Ağ Oluşturma ---
|
| 78 |
+
print("\n--- 3. Konsept Ağı Oluşturma ---"); start_time = time.time()
|
| 79 |
+
# GÜNCELLEME: Ağ nesnesini değişkende tut
|
| 80 |
+
concept_network = build_concept_network(similarity_threshold=0.60)
|
| 81 |
+
if concept_network is not None:
|
| 82 |
+
print("\n--- Oluşturulan Ağ Bilgileri ---")
|
| 83 |
+
print(f"Düğüm Sayısı (Konseptler): {concept_network.number_of_nodes()}")
|
| 84 |
+
print(f"Kenar Sayısı (İlişkiler/Benzerlikler): {concept_network.number_of_edges()}")
|
| 85 |
+
print(f"Ağ başarıyla oluşturuldu ve kaydedildi.")
|
| 86 |
+
else:
|
| 87 |
+
print("Konsept ağı oluşturulamadı.")
|
| 88 |
+
print(f"--- Ağ Oluşturma Tamamlandı. Süre: {time.time() - start_time:.2f} saniye ---")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
# --- YENİ: 4. Ağ Analizi (Metrik Hesaplama) ---
|
| 92 |
+
print("\n--- 4. Ağ Analizi Metrikleri ---"); start_time = time.time()
|
| 93 |
+
if concept_network is not None and concept_network.number_of_nodes() > 0:
|
| 94 |
+
network_analysis_df = get_network_analysis_results(concept_network)
|
| 95 |
+
if network_analysis_df is not None and not network_analysis_df.empty:
|
| 96 |
+
# Sonuçları kaydet
|
| 97 |
+
save_network_analysis(network_analysis_df)
|
| 98 |
+
print("Ağ metrikleri hesaplandı ve kaydedildi.")
|
| 99 |
+
# En yüksek derece merkeziyetine sahip ilk 10 konsepti göster
|
| 100 |
+
print("\n--- En Merkezi Konseptler (Degree Centrality Top 10) ---")
|
| 101 |
+
print(network_analysis_df.sort_values(by='degree_centrality', ascending=False).head(10).to_string(index=False))
|
| 102 |
+
else:
|
| 103 |
+
print("Ağ metrikleri hesaplanamadı veya sonuç boş.")
|
| 104 |
+
else:
|
| 105 |
+
print("Ağ analizi yapmak için geçerli bir ağ bulunamadı.")
|
| 106 |
+
print(f"--- Ağ Analizi Tamamlandı. Süre: {time.time() - start_time:.2f} saniye ---")
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# --- YENİ SIRA: 5. Ağ Görselleştirme ---
|
| 110 |
+
print("\n--- 5. Ağ Görselleştirmesi Oluşturma ---"); start_time = time.time()
|
| 111 |
+
visualization_path = None
|
| 112 |
+
if concept_network is not None:
|
| 113 |
+
# GÜNCELLEME: Analiz sonuçlarını da görselleştirmeye gönderebiliriz (ileride plotting.py'ı güncelleyince)
|
| 114 |
+
# Şimdilik sadece grafı gönderiyoruz.
|
| 115 |
+
visualization_path = visualize_network(graph=concept_network, output_filename="concept_network_visualization.html")
|
| 116 |
+
if visualization_path:
|
| 117 |
+
print(f"\nBaşarılı! İnteraktif ağ görselleştirmesi oluşturuldu:\n-> {visualization_path}")
|
| 118 |
+
print("\nBu HTML dosyasını web tarayıcınızda açarak ağı inceleyebilirsiniz.")
|
| 119 |
+
else: print("Ağ görselleştirmesi oluşturulurken bir sorun oluştu.")
|
| 120 |
+
else: print("Ağ oluşturulamadığı için görselleştirme yapılamıyor.")
|
| 121 |
+
print(f"--- Ağ Görselleştirme Tamamlandı. Süre: {time.time() - start_time:.2f} saniye ---")
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
overall_end_time = time.time()
|
| 125 |
+
print(f"\n<<< Tüm İşlemler Tamamlandı. Toplam Süre: {overall_end_time - overall_start_time:.2f} saniye >>>")
|
run_extractor.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
# src klasöründeki modüllerimize erişmek için
|
| 3 |
+
from src.extraction.extractor import process_documents_for_extraction
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
print(">>> Bilgi çıkarıcı çalıştırılıyor...")
|
| 7 |
+
print("Not: Bu işlem dokümanların uzunluğuna ve sayısına göre biraz zaman alabilir.")
|
| 8 |
+
start_time = time.time()
|
| 9 |
+
|
| 10 |
+
# Ana çıkarım fonksiyonumuzu çağırıyoruz
|
| 11 |
+
process_documents_for_extraction()
|
| 12 |
+
|
| 13 |
+
end_time = time.time()
|
| 14 |
+
print(f"<<< Bilgi çıkarıcı tamamlandı. Süre: {end_time - start_time:.2f} saniye.")
|
| 15 |
+
print(f"Kontrol edilmesi gereken dosyalar: data/processed_data/ klasöründeki concepts.parquet, mentions.parquet, relationships.parquet ve güncellenmiş documents.parquet")
|
run_loader.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
# src klasöründeki modüllerimize erişmek için
|
| 3 |
+
from src.data_management.loaders import process_raw_documents
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
print(">>> Veri yükleyici çalıştırılıyor...")
|
| 7 |
+
start_time = time.time()
|
| 8 |
+
|
| 9 |
+
# Ana işlem fonksiyonumuzu çağırıyoruz
|
| 10 |
+
process_raw_documents()
|
| 11 |
+
|
| 12 |
+
end_time = time.time()
|
| 13 |
+
print(f"<<< Veri yükleyici tamamlandı. Süre: {end_time - start_time:.2f} saniye.")
|
| 14 |
+
print(f"Kontrol edilmesi gereken dosya: data/processed_data/documents.parquet")
|
similarity.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/analysis/similarity.py
|
| 2 |
+
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
from sentence_transformers import SentenceTransformer
|
| 6 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 7 |
+
import logging
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# Yerel modüller
|
| 11 |
+
from src.data_management import storage
|
| 12 |
+
|
| 13 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 14 |
+
|
| 15 |
+
# Benzerlik matrisini kaydetmek için dosya adı
|
| 16 |
+
SIMILARITY_FILENAME = "concept_similarities"
|
| 17 |
+
EMBEDDINGS_FILENAME = "concept_embeddings" # Vektörleri de kaydedebiliriz
|
| 18 |
+
|
| 19 |
+
def calculate_concept_embeddings(model_name: str = 'all-MiniLM-L6-v2', force_recalculate: bool = False) -> dict[str, np.ndarray] | None:
|
| 20 |
+
"""
|
| 21 |
+
Her konsept için ortalama embedding vektörünü hesaplar.
|
| 22 |
+
Mention'ların context_snippet'lerini kullanır.
|
| 23 |
+
Hesaplanmış embedding'leri yüklemeye çalışır, yoksa hesaplar.
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
model_name (str): Kullanılacak Sentence Transformer modeli.
|
| 27 |
+
force_recalculate (bool): Daha önce hesaplanmış olsa bile yeniden hesaplamaya zorla.
|
| 28 |
+
|
| 29 |
+
Returns:
|
| 30 |
+
dict[str, np.ndarray] | None: Concept ID -> Ortalama Embedding Vektörü sözlüğü veya hata durumunda None.
|
| 31 |
+
"""
|
| 32 |
+
embeddings_filepath = storage.DATA_PATH / f"{EMBEDDINGS_FILENAME}.pkl" # Pickle ile saklayalım
|
| 33 |
+
|
| 34 |
+
if not force_recalculate and embeddings_filepath.exists():
|
| 35 |
+
try:
|
| 36 |
+
embeddings = pd.read_pickle(embeddings_filepath)
|
| 37 |
+
logging.info(f"Önceden hesaplanmış embedding'ler '{embeddings_filepath}' dosyasından yüklendi.")
|
| 38 |
+
# Dosyadan yüklenen bir sözlük olmalı
|
| 39 |
+
if isinstance(embeddings, dict):
|
| 40 |
+
return embeddings
|
| 41 |
+
else:
|
| 42 |
+
logging.warning("Yüklenen embedding dosyası beklenen formatta (dict) değil. Yeniden hesaplanacak.")
|
| 43 |
+
except Exception as e:
|
| 44 |
+
logging.error(f"Embedding'ler yüklenirken hata: {e}. Yeniden hesaplanacak.")
|
| 45 |
+
|
| 46 |
+
logging.info("Konsept embedding'leri hesaplanıyor...")
|
| 47 |
+
mentions_df = storage.load_dataframe('mentions', storage.MENTION_COLUMNS)
|
| 48 |
+
|
| 49 |
+
if mentions_df is None or mentions_df.empty:
|
| 50 |
+
logging.warning("Hesaplama için mention verisi bulunamadı.")
|
| 51 |
+
return None
|
| 52 |
+
|
| 53 |
+
# Geçerli context snippet'i olan mention'ları al
|
| 54 |
+
mentions_df.dropna(subset=['context_snippet', 'concept_id'], inplace=True)
|
| 55 |
+
if mentions_df.empty:
|
| 56 |
+
logging.warning("Geçerli context snippet bulunamadı.")
|
| 57 |
+
return None
|
| 58 |
+
|
| 59 |
+
# Modeli yükle (ilk seferde internetten indirilebilir)
|
| 60 |
+
try:
|
| 61 |
+
model = SentenceTransformer(model_name)
|
| 62 |
+
logging.info(f"Sentence Transformer modeli '{model_name}' yüklendi.")
|
| 63 |
+
except Exception as e:
|
| 64 |
+
logging.exception(f"Sentence Transformer modeli '{model_name}' yüklenirken hata: {e}")
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
# Konseptlere göre grupla
|
| 68 |
+
grouped_mentions = mentions_df.groupby('concept_id')['context_snippet'].apply(list)
|
| 69 |
+
|
| 70 |
+
concept_embeddings = {}
|
| 71 |
+
logging.info(f"{len(grouped_mentions)} konsept için embedding hesaplanacak...")
|
| 72 |
+
|
| 73 |
+
# Her konsept için embedding'leri hesapla ve ortalamasını al
|
| 74 |
+
for concept_id, snippets in grouped_mentions.items():
|
| 75 |
+
if not snippets: continue # Boş snippet listesi varsa atla
|
| 76 |
+
try:
|
| 77 |
+
# Tüm snippet'ların embedding'lerini tek seferde hesapla (daha verimli)
|
| 78 |
+
embeddings = model.encode(snippets, show_progress_bar=False) # İlerleme çubuğunu kapat
|
| 79 |
+
# Ortalama embedding'i hesapla
|
| 80 |
+
avg_embedding = np.mean(embeddings, axis=0)
|
| 81 |
+
concept_embeddings[concept_id] = avg_embedding
|
| 82 |
+
except Exception as e:
|
| 83 |
+
logging.error(f"Concept ID {concept_id} için embedding hesaplanırken hata: {e}")
|
| 84 |
+
continue # Bu konsepti atla
|
| 85 |
+
|
| 86 |
+
# Hesaplanan embedding'leri kaydet
|
| 87 |
+
try:
|
| 88 |
+
storage.DATA_PATH.mkdir(parents=True, exist_ok=True)
|
| 89 |
+
pd.to_pickle(concept_embeddings, embeddings_filepath)
|
| 90 |
+
logging.info(f"Hesaplanan embedding'ler '{embeddings_filepath}' dosyasına kaydedildi.")
|
| 91 |
+
except Exception as e:
|
| 92 |
+
logging.error(f"Embedding'ler kaydedilirken hata: {e}")
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
logging.info(f"{len(concept_embeddings)} konsept için ortalama embedding hesaplandı.")
|
| 96 |
+
return concept_embeddings
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def calculate_similarity_matrix(concept_embeddings: dict, force_recalculate: bool = False) -> pd.DataFrame | None:
|
| 100 |
+
"""
|
| 101 |
+
Verilen embedding vektörleri arasındaki kosinüs benzerliğini hesaplar.
|
| 102 |
+
Hesaplanmış benzerlikleri yüklemeye çalışır, yoksa hesaplar.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
concept_embeddings (dict[str, np.ndarray]): Concept ID -> Embedding Vektörü sözlüğü.
|
| 106 |
+
force_recalculate (bool): Daha önce hesaplanmış olsa bile yeniden hesaplamaya zorla.
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
pd.DataFrame | None: 'concept_id_1', 'concept_id_2', 'similarity' sütunlarını
|
| 110 |
+
içeren DataFrame veya hata durumunda None.
|
| 111 |
+
"""
|
| 112 |
+
similarity_filepath = storage.DATA_PATH / f"{SIMILARITY_FILENAME}.parquet"
|
| 113 |
+
|
| 114 |
+
if not force_recalculate and similarity_filepath.exists():
|
| 115 |
+
try:
|
| 116 |
+
similarity_df = storage.load_dataframe(SIMILARITY_FILENAME, ['concept_id_1', 'concept_id_2', 'similarity'])
|
| 117 |
+
logging.info(f"Önceden hesaplanmış benzerlik matrisi '{similarity_filepath}' dosyasından yüklendi.")
|
| 118 |
+
if similarity_df is not None and not similarity_df.empty:
|
| 119 |
+
return similarity_df
|
| 120 |
+
else:
|
| 121 |
+
logging.warning("Yüklenen benzerlik dosyası boş veya hatalı. Yeniden hesaplanacak.")
|
| 122 |
+
except Exception as e:
|
| 123 |
+
logging.error(f"Benzerlik matrisi yüklenirken hata: {e}. Yeniden hesaplanacak.")
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
if not concept_embeddings:
|
| 127 |
+
logging.error("Benzerlik hesaplamak için embedding verisi bulunamadı.")
|
| 128 |
+
return None
|
| 129 |
+
|
| 130 |
+
logging.info("Konseptler arası benzerlik matrisi hesaplanıyor...")
|
| 131 |
+
|
| 132 |
+
# Sözlükten sıralı liste ve matris oluştur
|
| 133 |
+
concept_ids = list(concept_embeddings.keys())
|
| 134 |
+
embedding_matrix = np.array(list(concept_embeddings.values()))
|
| 135 |
+
|
| 136 |
+
# Boyut kontrolü
|
| 137 |
+
if embedding_matrix.ndim != 2 or embedding_matrix.shape[0] != len(concept_ids):
|
| 138 |
+
logging.error(f"Embedding matrisinin boyutları ({embedding_matrix.shape}) beklenenden farklı.")
|
| 139 |
+
return None
|
| 140 |
+
|
| 141 |
+
# Kosinüs benzerliğini hesapla
|
| 142 |
+
try:
|
| 143 |
+
similarity_matrix = cosine_similarity(embedding_matrix)
|
| 144 |
+
except Exception as e:
|
| 145 |
+
logging.exception(f"Kosinüs benzerliği hesaplanırken hata: {e}")
|
| 146 |
+
return None
|
| 147 |
+
|
| 148 |
+
# Matrisi DataFrame'e dönüştür (uzun format)
|
| 149 |
+
similarity_data = []
|
| 150 |
+
num_concepts = len(concept_ids)
|
| 151 |
+
for i in range(num_concepts):
|
| 152 |
+
for j in range(i + 1, num_concepts): # Sadece üçgenin üstünü al (j > i) ve kendini (i=j) atla
|
| 153 |
+
similarity_data.append({
|
| 154 |
+
'concept_id_1': concept_ids[i],
|
| 155 |
+
'concept_id_2': concept_ids[j],
|
| 156 |
+
'similarity': similarity_matrix[i, j]
|
| 157 |
+
})
|
| 158 |
+
|
| 159 |
+
similarity_df = pd.DataFrame(similarity_data)
|
| 160 |
+
|
| 161 |
+
if similarity_df.empty:
|
| 162 |
+
logging.warning("Hesaplama sonucu benzerlik verisi üretilemedi.")
|
| 163 |
+
# Boş DataFrame kaydetmeyelim, None döndürelim
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
# Hesaplanan benzerlikleri kaydet
|
| 167 |
+
storage.save_dataframe(similarity_df, SIMILARITY_FILENAME)
|
| 168 |
+
|
| 169 |
+
logging.info(f"Benzerlik matrisi hesaplandı ve kaydedildi. {len(similarity_df)} çift.")
|
| 170 |
+
return similarity_df
|
storage.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/data_management/storage.py (TÜM SABİTLERİ İÇEREN DOĞRU TAM KOD)
|
| 2 |
+
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import logging
|
| 6 |
+
import uuid
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
import networkx as nx
|
| 9 |
+
import pickle
|
| 10 |
+
import string
|
| 11 |
+
|
| 12 |
+
# Temel veri klasörünün yolu
|
| 13 |
+
DATA_PATH = Path("data/processed_data")
|
| 14 |
+
# NetworkX graf dosyalarının yolu
|
| 15 |
+
NETWORK_PATH = Path("output/networks")
|
| 16 |
+
|
| 17 |
+
# --- TÜM GEREKLİ SABİT TANIMLARI ---
|
| 18 |
+
FREQUENCY_FILENAME = "analysis_concept_frequencies"
|
| 19 |
+
SIMILARITY_FILENAME = "analysis_concept_similarities"
|
| 20 |
+
NETWORK_ANALYSIS_FILENAME = "analysis_network_results"
|
| 21 |
+
GRAPH_FILENAME = "concept_network"
|
| 22 |
+
EMBEDDINGS_FILENAME = "concept_embeddings"
|
| 23 |
+
# ------------------------------------
|
| 24 |
+
|
| 25 |
+
# DataFrame sütun isimleri
|
| 26 |
+
DOC_COLUMNS = ['doc_id', 'filepath', 'publication_date', 'status', 'processed_text_path']
|
| 27 |
+
CONCEPT_COLUMNS = ['concept_id', 'name', 'aliases']
|
| 28 |
+
MENTION_COLUMNS = ['mention_id', 'doc_id', 'concept_id', 'context_snippet', 'start_char', 'end_char']
|
| 29 |
+
RELATIONSHIP_COLUMNS = ['relationship_id', 'source_concept_id', 'target_concept_id', 'type', 'mention_id', 'doc_id', 'sentence']
|
| 30 |
+
NETWORK_ANALYSIS_COLUMNS = ['concept_id', 'name', 'degree_centrality', 'betweenness_centrality', 'eigenvector_centrality', 'community_id']
|
| 31 |
+
|
| 32 |
+
# Logging ayarları
|
| 33 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 34 |
+
|
| 35 |
+
# --- DataFrame Yükleme/Kaydetme (Değişiklik yok) ---
|
| 36 |
+
def load_dataframe(filename: str, columns: list) -> pd.DataFrame:
|
| 37 |
+
filepath = DATA_PATH / f"{filename}.parquet"
|
| 38 |
+
if filepath.exists():
|
| 39 |
+
try:
|
| 40 |
+
df = pd.read_parquet(filepath)
|
| 41 |
+
logging.info(f"'{filepath}' başarıyla yüklendi.")
|
| 42 |
+
if columns: # Check columns only if a list is provided
|
| 43 |
+
for col in columns:
|
| 44 |
+
if col not in df.columns:
|
| 45 |
+
logging.warning(f"'{filepath}' dosyasında '{col}' sütunu eksik. Ekleniyor...")
|
| 46 |
+
df[col] = None
|
| 47 |
+
return df
|
| 48 |
+
except Exception as e:
|
| 49 |
+
logging.error(f"'{filepath}' yüklenirken hata oluştu: {e}")
|
| 50 |
+
return pd.DataFrame(columns=columns if columns else None)
|
| 51 |
+
else:
|
| 52 |
+
logging.info(f"'{filepath}' bulunamadı. Boş DataFrame oluşturuluyor.")
|
| 53 |
+
return pd.DataFrame(columns=columns if columns else None)
|
| 54 |
+
|
| 55 |
+
def save_dataframe(df: pd.DataFrame, filename: str):
|
| 56 |
+
DATA_PATH.mkdir(parents=True, exist_ok=True)
|
| 57 |
+
filepath = DATA_PATH / f"{filename}.parquet"
|
| 58 |
+
try:
|
| 59 |
+
for col in df.select_dtypes(include=['object']).columns:
|
| 60 |
+
if df[col].map(type).isin([list, dict, datetime, pd.Timestamp]).any(): continue
|
| 61 |
+
df[col] = df[col].where(pd.notnull(df[col]), None)
|
| 62 |
+
try: df[col] = df[col].astype(pd.StringDtype())
|
| 63 |
+
except TypeError: logging.debug(f"Sütun '{col}' StringDtype'a çevrilemedi, orijinal tip korunuyor.")
|
| 64 |
+
df.to_parquet(filepath, index=False)
|
| 65 |
+
logging.info(f"DataFrame başarıyla '{filepath}' olarak kaydedildi.")
|
| 66 |
+
except Exception as e:
|
| 67 |
+
logging.error(f"DataFrame '{filepath}' olarak kaydedilirken hata oluştu: {e}")
|
| 68 |
+
|
| 69 |
+
# --- Doküman Yönetimi (Değişiklik yok) ---
|
| 70 |
+
def add_document(filepath_str: str, publication_date) -> str | None:
|
| 71 |
+
documents_df = load_dataframe('documents', DOC_COLUMNS)
|
| 72 |
+
filepath_str = str(Path(filepath_str).resolve())
|
| 73 |
+
existing_doc = documents_df[documents_df['filepath'] == filepath_str]
|
| 74 |
+
if not existing_doc.empty:
|
| 75 |
+
existing_doc_id = existing_doc['doc_id'].iloc[0]
|
| 76 |
+
logging.warning(f"Doküman zaten kayıtlı: {filepath_str} (ID: {existing_doc_id})")
|
| 77 |
+
return str(existing_doc_id)
|
| 78 |
+
new_doc_id = str(uuid.uuid4())
|
| 79 |
+
try: pub_date_obj = pd.to_datetime(publication_date).date()
|
| 80 |
+
except ValueError: logging.error(f"Geçersiz tarih formatı: {publication_date}. None olarak kaydedilecek."); pub_date_obj = None
|
| 81 |
+
new_document_data = {'doc_id': new_doc_id, 'filepath': filepath_str, 'publication_date': pub_date_obj, 'status': 'added', 'processed_text_path': None}
|
| 82 |
+
new_row_df = pd.DataFrame([new_document_data])
|
| 83 |
+
if pub_date_obj is not None: new_row_df['publication_date'] = pd.to_datetime(new_row_df['publication_date']); dtype_dict = {'publication_date': 'datetime64[s]'}
|
| 84 |
+
else: dtype_dict = {}
|
| 85 |
+
documents_df = pd.concat([documents_df, new_row_df], ignore_index=True)
|
| 86 |
+
for col, dtype in dtype_dict.items():
|
| 87 |
+
try: documents_df[col] = documents_df[col].astype(dtype)
|
| 88 |
+
except TypeError: logging.warning(f"Sütun '{col}' tipi '{dtype}' olarak ayarlanamadı.")
|
| 89 |
+
save_dataframe(documents_df, 'documents')
|
| 90 |
+
logging.info(f"Yeni doküman eklendi: {filepath_str} (ID: {new_doc_id})")
|
| 91 |
+
return new_doc_id
|
| 92 |
+
|
| 93 |
+
def update_document_status(doc_id: str, new_status: str, text_path: str | None = None):
|
| 94 |
+
docs_df = load_dataframe('documents', DOC_COLUMNS)
|
| 95 |
+
doc_index = docs_df[docs_df['doc_id'] == doc_id].index
|
| 96 |
+
if not doc_index.empty:
|
| 97 |
+
idx = doc_index[0]
|
| 98 |
+
docs_df.loc[idx, 'status'] = new_status
|
| 99 |
+
if text_path: docs_df.loc[idx, 'processed_text_path'] = text_path
|
| 100 |
+
save_dataframe(docs_df, 'documents')
|
| 101 |
+
logging.info(f"Doküman durumu güncellendi: ID {doc_id} -> {new_status}")
|
| 102 |
+
else: logging.warning(f"Durumu güncellenecek doküman bulunamadı: ID {doc_id}")
|
| 103 |
+
|
| 104 |
+
# --- Konsept, Mention, İlişki Yönetimi (Değişiklik yok) ---
|
| 105 |
+
def add_concept(raw_name: str) -> str | None:
|
| 106 |
+
concepts_df = load_dataframe('concepts', CONCEPT_COLUMNS)
|
| 107 |
+
name = raw_name.lower().strip().strip(string.punctuation + string.whitespace)
|
| 108 |
+
if name.endswith("'s"): name = name[:-2].strip()
|
| 109 |
+
name = ' '.join(name.split())
|
| 110 |
+
if not name or len(name) < 2: return None
|
| 111 |
+
existing_concept = concepts_df[concepts_df['name'] == name]
|
| 112 |
+
if not existing_concept.empty: return str(existing_concept['concept_id'].iloc[0])
|
| 113 |
+
new_concept_id = str(uuid.uuid4()); new_concept_data = {'concept_id': new_concept_id, 'name': name, 'aliases': [raw_name]}
|
| 114 |
+
new_row_df = pd.DataFrame([new_concept_data]); concepts_df = pd.concat([concepts_df, new_row_df], ignore_index=True)
|
| 115 |
+
concepts_df['aliases'] = concepts_df['aliases'].astype('object')
|
| 116 |
+
save_dataframe(concepts_df, 'concepts')
|
| 117 |
+
logging.info(f"Yeni konsept eklendi: '{name}' (Orijinal: '{raw_name}', ID: {new_concept_id})")
|
| 118 |
+
return new_concept_id
|
| 119 |
+
|
| 120 |
+
def add_mention(doc_id: str, concept_id: str, context: str, start: int, end: int) -> str | None:
|
| 121 |
+
if concept_id is None: return None
|
| 122 |
+
mentions_df = load_dataframe('mentions', MENTION_COLUMNS); new_mention_id = str(uuid.uuid4())
|
| 123 |
+
new_mention_data = {'mention_id': new_mention_id, 'doc_id': doc_id, 'concept_id': concept_id, 'context_snippet': context[:500], 'start_char': start, 'end_char': end}
|
| 124 |
+
new_row_df = pd.DataFrame([new_mention_data]); mentions_df = pd.concat([mentions_df, new_row_df], ignore_index=True)
|
| 125 |
+
save_dataframe(mentions_df, 'mentions'); return new_mention_id
|
| 126 |
+
|
| 127 |
+
def add_relationship(source_concept_id: str, target_concept_id: str, rel_type: str, mention_id: str | None, doc_id: str, sentence: str) -> str | None:
|
| 128 |
+
if source_concept_id is None or target_concept_id is None: return None
|
| 129 |
+
relationships_df = load_dataframe('relationships', RELATIONSHIP_COLUMNS); new_relationship_id = str(uuid.uuid4())
|
| 130 |
+
new_relationship_data = {'relationship_id': new_relationship_id, 'source_concept_id': source_concept_id, 'target_concept_id': target_concept_id, 'type': rel_type, 'mention_id': mention_id, 'doc_id': doc_id, 'sentence': sentence[:500]}
|
| 131 |
+
new_row_df = pd.DataFrame([new_relationship_data]); relationships_df = pd.concat([relationships_df, new_row_df], ignore_index=True)
|
| 132 |
+
save_dataframe(relationships_df, 'relationships'); return new_relationship_id
|
| 133 |
+
|
| 134 |
+
# --- NetworkX Graf Yükleme/Kaydetme (Değişiklik yok) ---
|
| 135 |
+
def save_network(graph: nx.Graph, filename: str):
|
| 136 |
+
NETWORK_PATH.mkdir(parents=True, exist_ok=True); filepath = NETWORK_PATH / f"{filename}.pkl"
|
| 137 |
+
try:
|
| 138 |
+
with open(filepath, 'wb') as f: pickle.dump(graph, f)
|
| 139 |
+
logging.info(f"NetworkX grafı başarıyla '{filepath}' olarak kaydedildi.")
|
| 140 |
+
except Exception as e: logging.error(f"Graf '{filepath}' olarak kaydedilirken hata: {e}")
|
| 141 |
+
|
| 142 |
+
def load_network(filename: str) -> nx.Graph | None:
|
| 143 |
+
filepath = NETWORK_PATH / f"{filename}.pkl"
|
| 144 |
+
if filepath.exists():
|
| 145 |
+
try:
|
| 146 |
+
with open(filepath, 'rb') as f: graph = pickle.load(f)
|
| 147 |
+
logging.info(f"NetworkX grafı '{filepath}' başarıyla yüklendi.")
|
| 148 |
+
return graph
|
| 149 |
+
except Exception as e: logging.error(f"Graf '{filepath}' yüklenirken hata: {e}"); return nx.Graph()
|
| 150 |
+
else: logging.warning(f"Graf dosyası bulunamadı: '{filepath}'"); return nx.Graph()
|
temporal.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/analysis/temporal.py (Yarı ömür fonksiyonu eklendi)
|
| 2 |
+
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
from scipy.optimize import curve_fit
|
| 6 |
+
import logging
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
# Yerel modüllerimizi içe aktaralım
|
| 11 |
+
from src.data_management import storage
|
| 12 |
+
|
| 13 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 14 |
+
|
| 15 |
+
def calculate_concept_frequencies(time_period: str = 'Y') -> pd.DataFrame | None:
|
| 16 |
+
"""
|
| 17 |
+
Konseptlerin zaman içindeki kullanım sıklıklarını hesaplar. (Önceki kodla aynı)
|
| 18 |
+
"""
|
| 19 |
+
logging.info(f"Konsept frekansları '{time_period}' periyodu için hesaplanıyor...")
|
| 20 |
+
mentions_df = storage.load_dataframe('mentions', storage.MENTION_COLUMNS)
|
| 21 |
+
documents_df = storage.load_dataframe('documents', storage.DOC_COLUMNS)
|
| 22 |
+
|
| 23 |
+
if mentions_df is None or documents_df is None:
|
| 24 |
+
logging.error("Mention veya Document verisi yüklenemedi. Frekans hesaplanamıyor.")
|
| 25 |
+
return None
|
| 26 |
+
if mentions_df.empty:
|
| 27 |
+
logging.warning("Mention verisi boş. Frekans hesaplanamıyor.")
|
| 28 |
+
return pd.DataFrame(columns=['concept_id', 'time_period_start', 'frequency'])
|
| 29 |
+
if documents_df.empty:
|
| 30 |
+
logging.warning("Document verisi boş. Tarih bilgisi alınamıyor, frekans hesaplanamıyor.")
|
| 31 |
+
return pd.DataFrame(columns=['concept_id', 'time_period_start', 'frequency'])
|
| 32 |
+
|
| 33 |
+
docs_subset = documents_df[['doc_id', 'publication_date']].copy()
|
| 34 |
+
try:
|
| 35 |
+
docs_subset['publication_date'] = pd.to_datetime(docs_subset['publication_date'], errors='coerce')
|
| 36 |
+
except Exception as e:
|
| 37 |
+
logging.error(f"Dokümanlardaki 'publication_date' sütunu datetime'a çevrilemedi: {e}")
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
original_doc_count = len(docs_subset)
|
| 41 |
+
docs_subset.dropna(subset=['publication_date'], inplace=True)
|
| 42 |
+
valid_date_count = len(docs_subset)
|
| 43 |
+
if original_doc_count > valid_date_count:
|
| 44 |
+
logging.warning(f"{original_doc_count - valid_date_count} dokümanın geçerli yayın tarihi yok, frekans hesaplamasına dahil edilmeyecek.")
|
| 45 |
+
|
| 46 |
+
if docs_subset.empty:
|
| 47 |
+
logging.warning("Geçerli yayın tarihine sahip doküman bulunamadı. Frekans hesaplanamıyor.")
|
| 48 |
+
return pd.DataFrame(columns=['concept_id', 'time_period_start', 'frequency'])
|
| 49 |
+
|
| 50 |
+
mentions_with_dates = pd.merge(mentions_df, docs_subset, on='doc_id', how='inner')
|
| 51 |
+
|
| 52 |
+
if mentions_with_dates.empty:
|
| 53 |
+
logging.warning("Mention'lar ile doküman tarihleri birleştirilemedi veya sonuç boş.")
|
| 54 |
+
return pd.DataFrame(columns=['concept_id', 'time_period_start', 'frequency'])
|
| 55 |
+
|
| 56 |
+
logging.info(f"{len(mentions_with_dates)} mention için tarih bilgisi bulundu.")
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
frequency_df = mentions_with_dates.groupby(
|
| 60 |
+
['concept_id', pd.Grouper(key='publication_date', freq=time_period)]
|
| 61 |
+
).size().reset_index(name='frequency')
|
| 62 |
+
frequency_df.rename(columns={'publication_date': 'time_period_start'}, inplace=True)
|
| 63 |
+
logging.info(f"Frekans hesaplaması tamamlandı. {len(frequency_df)} satır sonuç üretildi.")
|
| 64 |
+
frequency_df.sort_values(by=['concept_id', 'time_period_start'], inplace=True)
|
| 65 |
+
return frequency_df
|
| 66 |
+
except Exception as e:
|
| 67 |
+
logging.exception(f"Frekans hesaplanırken hata oluştu: {e}")
|
| 68 |
+
return None
|
| 69 |
+
|
| 70 |
+
# --- YENİ: Yarı Ömür Hesaplama ---
|
| 71 |
+
|
| 72 |
+
def exponential_decay(t, A, decay_rate):
|
| 73 |
+
"""Üstel bozulma fonksiyonu: A * exp(-decay_rate * t)."""
|
| 74 |
+
# Decay rate negatif olmamalı (bozunma varsayımı)
|
| 75 |
+
decay_rate = max(0, decay_rate) # Negatifse sıfır yap
|
| 76 |
+
return A * np.exp(-decay_rate * t)
|
| 77 |
+
|
| 78 |
+
def calculate_half_life(concept_id: str,
|
| 79 |
+
frequency_df: pd.DataFrame,
|
| 80 |
+
concept_name: str | None = None,
|
| 81 |
+
min_data_points: int = 4,
|
| 82 |
+
min_decay_rate: float = 1e-6) -> float | None:
|
| 83 |
+
"""
|
| 84 |
+
Verilen konsept için frekans verisine üstel bozulma modeli uygulayarak
|
| 85 |
+
yarı ömrü (yıl olarak) hesaplar.
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
concept_id (str): Hesaplanacak konseptin ID'si.
|
| 89 |
+
frequency_df (pd.DataFrame): calculate_concept_frequencies'ten dönen DataFrame.
|
| 90 |
+
('concept_id', 'time_period_start', 'frequency' sütunları olmalı).
|
| 91 |
+
concept_name (str | None): Loglama için konseptin adı (opsiyonel).
|
| 92 |
+
min_data_points (int): Yarı ömür hesaplamak için gereken minimum zaman noktası sayısı.
|
| 93 |
+
min_decay_rate (float): Kabul edilebilir minimum bozunma oranı (çok küçükse yarı ömür sonsuz kabul edilir).
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
float | None: Hesaplanan yarı ömür (yıl olarak) veya hesaplanamazsa None.
|
| 97 |
+
np.inf dönebilir eğer bozunma oranı çok küçükse.
|
| 98 |
+
"""
|
| 99 |
+
log_prefix = f"Yarı Ömür ({concept_name or concept_id}):"
|
| 100 |
+
|
| 101 |
+
if frequency_df is None or frequency_df.empty:
|
| 102 |
+
logging.warning(f"{log_prefix} Frekans verisi boş.")
|
| 103 |
+
return None
|
| 104 |
+
|
| 105 |
+
# Konsepte ait veriyi filtrele ve zamana göre sırala
|
| 106 |
+
concept_data = frequency_df[frequency_df['concept_id'] == concept_id].sort_values(by='time_period_start').copy()
|
| 107 |
+
|
| 108 |
+
# Yeterli veri noktası var mı?
|
| 109 |
+
if len(concept_data) < min_data_points:
|
| 110 |
+
logging.info(f"{log_prefix} Yeterli veri noktası yok ({len(concept_data)} < {min_data_points}). Hesaplama yapılamıyor.")
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
# Zamanı sayısal değere çevir (ilk yıldan itibaren geçen yıl sayısı)
|
| 114 |
+
try:
|
| 115 |
+
# İlk zaman noktasını t=0 kabul et
|
| 116 |
+
start_date = concept_data['time_period_start'].min()
|
| 117 |
+
# Zaman farkını gün olarak hesapla ve yıla çevir
|
| 118 |
+
concept_data['time_elapsed_years'] = (concept_data['time_period_start'] - start_date).dt.days / 365.25
|
| 119 |
+
except Exception as e:
|
| 120 |
+
logging.error(f"{log_prefix} Zaman farkı hesaplanırken hata: {e}")
|
| 121 |
+
return None
|
| 122 |
+
|
| 123 |
+
time_values = concept_data['time_elapsed_years'].values
|
| 124 |
+
frequency_values = concept_data['frequency'].values
|
| 125 |
+
|
| 126 |
+
# Frekanslar artıyor mu veya sabit mi kontrol et (basit kontrol)
|
| 127 |
+
# Eğer son değer ilk değerden büyükse veya tüm değerler aynıysa, bozunma yok kabul et
|
| 128 |
+
if frequency_values[-1] > frequency_values[0] or np.all(frequency_values == frequency_values[0]):
|
| 129 |
+
logging.info(f"{log_prefix} Veride belirgin bir azalma gözlenmedi. Yarı ömür hesaplanamıyor.")
|
| 130 |
+
return None # Veya np.inf? Şimdilik None.
|
| 131 |
+
|
| 132 |
+
# Modeli uydurmak için başlangıç tahminleri
|
| 133 |
+
initial_A_guess = frequency_values[0] # İlk frekans değeri
|
| 134 |
+
initial_lambda_guess = 0.1 # Küçük pozitif bir bozunma oranı tahmini
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
# curve_fit ile modeli verilere uydur
|
| 138 |
+
params, covariance = curve_fit(
|
| 139 |
+
exponential_decay,
|
| 140 |
+
time_values,
|
| 141 |
+
frequency_values,
|
| 142 |
+
p0=[initial_A_guess, initial_lambda_guess],
|
| 143 |
+
bounds=([0, 0], [np.inf, np.inf]) # Parametrelerin pozitif olmasını sağla
|
| 144 |
+
# maxfev artırılabilir eğer "Optimal parameters not found" hatası alınırsa
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
A_fit, decay_rate_fit = params
|
| 148 |
+
|
| 149 |
+
# Bozunma oranı anlamlı mı?
|
| 150 |
+
if decay_rate_fit < min_decay_rate:
|
| 151 |
+
logging.info(f"{log_prefix} Hesaplanan bozunma oranı ({decay_rate_fit:.4f}) çok düşük. Yarı ömür sonsuz kabul ediliyor.")
|
| 152 |
+
return np.inf # Sonsuz yarı ömür
|
| 153 |
+
|
| 154 |
+
# Yarı ömrü hesapla: ln(2) / decay_rate
|
| 155 |
+
half_life_years = np.log(2) / decay_rate_fit
|
| 156 |
+
logging.info(f"{log_prefix} Başarıyla hesaplandı. A={A_fit:.2f}, Bozunma Oranı={decay_rate_fit:.4f}, Yarı Ömür={half_life_years:.2f} yıl.")
|
| 157 |
+
return half_life_years
|
| 158 |
+
|
| 159 |
+
except RuntimeError as e:
|
| 160 |
+
logging.warning(f"{log_prefix} Üstel bozulma modeli uydurulamadı: {e}. Yarı ömür hesaplanamıyor.")
|
| 161 |
+
return None
|
| 162 |
+
except Exception as e:
|
| 163 |
+
logging.exception(f"{log_prefix} Yarı ömür hesaplanırken beklenmeyen hata: {e}")
|
| 164 |
+
return None
|
test1.png
ADDED
|