Buggy python submission (verdict: Time Limit Exceeded):
import sys
input = sys.stdin.readline


class UnionFindNode(object):
    """
    Union-Find構造
    ノードのグループ併合や、所属グループ判定を高速に処理する
    """

    def __init__(self, group_id, x, y, parent=None):
        self.group_id_ = group_id
        self.parent_ = parent
        self.size_ = 1
        self.rank_ = 1
        self.x_ = {x}
        self.y_ = {y}

    def __str__(self):
        template = "UnionFindNode(group_id: {}, \n\tparent: {}, size: {}, rank: {}, x: {}, y: {})"
        return template.format(self.group_id_, self.parent_, self.size_,
                               self.rank_, self.x_, self.y_)

    def is_root(self):
        return not self.parent_

    def root(self):
        parent = self
        while not parent.is_root():
            parent = parent.parent_
            self.parent_ = parent
        return parent

    def find(self):
        root = self.root()
        return root.group_id_

    def rank(self):
        root = self.root()
        return root.rank_

    def get_size(self):
        root = self.root()
        return root.size_

    def get_count(self):
        root = self.root()
        if len(root.x_) == 1 or len(root.y_) == 1:
            return 0
        return len(root.x_) * len(root.y_) - root.size_

    def unite(self, unite_node):
        root = self.root()
        unite_root = unite_node.root()

        if root.group_id_ != unite_root.group_id_:
            if root.rank() > unite_root.rank():
                unite_root.parent_ = root
                root.rank_ = max(root.rank_, unite_root.rank_ + 1)
                root.size_ += unite_root.size_
                root.x_ = root.x_ | unite_root.x_
                root.y_ = root.y_ | unite_root.y_
            else:
                root.parent_ = unite_root
                unite_root.rank_ = max(root.rank_ + 1, unite_root.rank_)
                unite_root.size_ += root.size_
                unite_root.x_ = root.x_ | unite_root.x_
                unite_root.y_ = root.y_ | unite_root.y_


N = int(input())
xy_array = [list(map(int, input().split())) for i in range(N)]

# print(xy_array)
node_array = [UnionFindNode(i, xy[0], xy[1]) for i, xy in enumerate(xy_array)]

for i in range(N):
    for j in range(i, N):
        if xy_array[i][0] == xy_array[j][0] or xy_array[i][1] == xy_array[j][1]:
            node_array[i].unite(node_array[j])

# print("\n".join(list(map(str, node_array))))
# print("\n".join(list(map(lambda x: str(x.root()), node_array))))

ans = 0
for node in node_array:
    if node.is_root():
        # print("root")
        ans += node.get_count()

print(ans)

Fixed submission:
import sys
input = sys.stdin.readline


class UnionFindNode(object):
    """
    Union-Find構造
    ノードのグループ併合や、所属グループ判定を高速に処理する
    """

    def __init__(self, group_id, x, y, parent=None):
        self.group_id_ = group_id
        self.parent_ = parent
        self.size_ = 1
        self.rank_ = 1

    def __str__(self):
        template = "UnionFindNode(group_id: {}, \n\tparent: {}, size: {}, rank: {})"
        return template.format(self.group_id_, self.parent_, self.size_,
                               self.rank_)

    def is_root(self):
        return not self.parent_

    def root(self):
        parent = self
        while not parent.is_root():
            parent = parent.parent_
            self.parent_ = parent
        return parent

    def find(self):
        root = self.root()
        return root.group_id_

    def rank(self):
        root = self.root()
        return root.rank_

    def get_size(self):
        root = self.root()
        return root.size_

    def unite(self, unite_node):
        root = self.root()
        unite_root = unite_node.root()

        if root.group_id_ != unite_root.group_id_:
            if root.rank() > unite_root.rank():
                unite_root.parent_ = root
                root.rank_ = max(root.rank_, unite_root.rank_ + 1)
                root.size_ += unite_root.size_

            else:
                root.parent_ = unite_root
                unite_root.rank_ = max(root.rank_ + 1, unite_root.rank_)
                unite_root.size_ += root.size_


N = int(input())
xy_array = []
for i in range(N):
    x, y = map(int, input().split())
    xy_array.append([i, x, y])
# print(xy_array)
node_array = [UnionFindNode(xy[0], xy[1], xy[2]) for xy in xy_array]

xsort_array = sorted(xy_array, key=lambda x: x[1])

for i in range(N - 1):
    if xsort_array[i][1] == xsort_array[i + 1][1]:
        node_array[xsort_array[i][0]].unite(node_array[xsort_array[i + 1][0]])

ysort_array = sorted(xy_array, key=lambda x: x[2])

for i in range(N - 1):
    if ysort_array[i][2] == ysort_array[i + 1][2]:
        node_array[ysort_array[i][0]].unite(node_array[ysort_array[i + 1][0]])

# print("\n".join(list(map(str, node_array))))
# print("\n".join(list(map(lambda x: str(x.root()), node_array))))

ans = 0

xy_group = [[set(), set()] for i in range(N)]
for node, xy in zip(node_array, xy_array):
    group = node.find()
    xy_group[group][0].add(xy[1])
    xy_group[group][1].add(xy[2])

ans = 0
for i, xy in enumerate(xy_group):
    if len(xy[0]) <= 1 or len(xy[1]) <= 1:
        continue
    ans += len(xy[0]) * len(xy[1]) - node_array[i].get_size()

print(ans)

Buggy python submission (verdict: Time Limit Exceeded):
import sys
input = sys.stdin.readline


class UnionFindNode(object):
    """
    Union-Find構造
    ノードのグループ併合や、所属グループ判定を高速に処理する
    """

    def __init__(self, group_id, x, y, parent=None):
        self.group_id_ = group_id
        self.parent_ = parent
        self.size_ = 1
        self.rank_ = 1
        self.x_ = {x}
        self.y_ = {y}

    def __str__(self):
        template = "UnionFindNode(group_id: {}, \n\tparent: {}, size: {}, rank: {}, x: {}, y: {})"
        return template.format(self.group_id_, self.parent_, self.size_,
                               self.rank_, self.x_, self.y_)

    def is_root(self):
        return not self.parent_

    def root(self):
        parent = self
        while not parent.is_root():
            parent = parent.parent_
            self.parent_ = parent
        return parent

    def find(self):
        root = self.root()
        return root.group_id_

    def rank(self):
        root = self.root()
        return root.rank_

    def get_size(self):
        root = self.root()
        return root.size_

    def get_count(self):
        root = self.root()
        if len(root.x_) == 1 or len(root.y_) == 1:
            return 0
        return len(root.x_) * len(root.y_) - root.size_

    def unite(self, unite_node):
        root = self.root()
        unite_root = unite_node.root()

        if root.group_id_ != unite_root.group_id_:
            if root.rank() > unite_root.rank():
                unite_root.parent_ = root
                root.rank_ = max(root.rank_, unite_root.rank_ + 1)
                root.size_ += unite_root.size_
                root.x_ = root.x_ | unite_root.x_
                root.y_ = root.y_ | unite_root.y_
            else:
                root.parent_ = unite_root
                unite_root.rank_ = max(root.rank_ + 1, unite_root.rank_)
                unite_root.size_ += root.size_
                unite_root.x_ = root.x_ | unite_root.x_
                unite_root.y_ = root.y_ | unite_root.y_


N = int(input())
xy_array = []
for i in range(N):
    x, y = map(int, input().split())
    xy_array.append([i, x, y])
# print(xy_array)
node_array = [UnionFindNode(xy[0], xy[1], xy[2]) for xy in xy_array]

xsort_array = sorted(xy_array, key=lambda x: x[1])

for i in range(N - 1):
    if xsort_array[i][1] == xsort_array[i + 1][1]:
        node_array[xsort_array[i][0]].unite(node_array[xsort_array[i + 1][0]])

ysort_array = sorted(xy_array, key=lambda x: x[2])

for i in range(N - 1):
    if ysort_array[i][2] == ysort_array[i + 1][2]:
        node_array[ysort_array[i][0]].unite(node_array[ysort_array[i + 1][0]])

# print("\n".join(list(map(str, node_array))))
# print("\n".join(list(map(lambda x: str(x.root()), node_array))))

ans = 0
for node in node_array:
    if node.is_root():
        # print("root")
        ans += node.get_count()

print(ans)

Fixed submission:
import sys
input = sys.stdin.readline


class UnionFindNode(object):
    """
    Union-Find構造
    ノードのグループ併合や、所属グループ判定を高速に処理する
    """

    def __init__(self, group_id, x, y, parent=None):
        self.group_id_ = group_id
        self.parent_ = parent
        self.size_ = 1
        self.rank_ = 1

    def __str__(self):
        template = "UnionFindNode(group_id: {}, \n\tparent: {}, size: {}, rank: {})"
        return template.format(self.group_id_, self.parent_, self.size_,
                               self.rank_)

    def is_root(self):
        return not self.parent_

    def root(self):
        parent = self
        while not parent.is_root():
            parent = parent.parent_
            self.parent_ = parent
        return parent

    def find(self):
        root = self.root()
        return root.group_id_

    def rank(self):
        root = self.root()
        return root.rank_

    def get_size(self):
        root = self.root()
        return root.size_

    def unite(self, unite_node):
        root = self.root()
        unite_root = unite_node.root()

        if root.group_id_ != unite_root.group_id_:
            if root.rank() > unite_root.rank():
                unite_root.parent_ = root
                root.rank_ = max(root.rank_, unite_root.rank_ + 1)
                root.size_ += unite_root.size_

            else:
                root.parent_ = unite_root
                unite_root.rank_ = max(root.rank_ + 1, unite_root.rank_)
                unite_root.size_ += root.size_


N = int(input())
xy_array = []
for i in range(N):
    x, y = map(int, input().split())
    xy_array.append([i, x, y])
# print(xy_array)
node_array = [UnionFindNode(xy[0], xy[1], xy[2]) for xy in xy_array]

xsort_array = sorted(xy_array, key=lambda x: x[1])

for i in range(N - 1):
    if xsort_array[i][1] == xsort_array[i + 1][1]:
        node_array[xsort_array[i][0]].unite(node_array[xsort_array[i + 1][0]])

ysort_array = sorted(xy_array, key=lambda x: x[2])

for i in range(N - 1):
    if ysort_array[i][2] == ysort_array[i + 1][2]:
        node_array[ysort_array[i][0]].unite(node_array[ysort_array[i + 1][0]])

# print("\n".join(list(map(str, node_array))))
# print("\n".join(list(map(lambda x: str(x.root()), node_array))))

ans = 0

xy_group = [[set(), set()] for i in range(N)]
for node, xy in zip(node_array, xy_array):
    group = node.find()
    xy_group[group][0].add(xy[1])
    xy_group[group][1].add(xy[2])

ans = 0
for i, xy in enumerate(xy_group):
    if len(xy[0]) <= 1 or len(xy[1]) <= 1:
        continue
    ans += len(xy[0]) * len(xy[1]) - node_array[i].get_size()

print(ans)