【Python金融データ】EDINET APIの使い方(第3回)

投資・ファイナンス

前回 、前々回とEDINET API を使って有価証券報告書を取得し解析する方法を紹介してきました。
今回は前回の続きとして、xbrl ファイルから財務諸表に関連する数値データをうまく取り出す方法を紹介していきます
これまでの内容の続きになりますのでまだ過去の記事を確認していない場合は、この記事を読む前に確認してみてください。
今回使用したコードはこちらのGoogleColabで確認できます。

ファイルの読み込み

前回 紹介した方法で XBRLファイルを読み込みます。

ctrl = Cntlr.Cntlr(logFileName='logToPrint')
modelXbrl = ctrl.modelManager.load(xbrl_file)
ViewFileFactTable.viewFacts(modelXbrl, 'test.csv', arcrole=XbrlConst.summationItem,cols=cols , lang='ja')
df = pd.read_csv('test.csv')


linkroleの取得

xbrl ファイルにはlinkroleと呼ばれる目次のような機能があります。
まずはこの項目を取得してどういった項目があるのか確認してみます。

# 関係性セットの取得
arcrole =  XbrlConst.parentChild
linkrole = None
linkqname = None
arcqname = None
labelrole = None
lang = "ja"
relationshipSet = modelXbrl.relationshipSet(arcrole, linkrole, linkqname, arcqname)


# 関係性の種類を取得してソート
linkroleUris = []
for linkroleUri in relationshipSet.linkRoleUris:
    modelRoleTypes = modelXbrl.roleTypes.get(linkroleUri)
    if modelRoleTypes:
        roledefinition = (modelRoleTypes[0].genLabel(lang=lang, strip=True) or modelRoleTypes[0].definition or linkroleUri)
    else:
        roledefinition = linkroleUri
    linkroleUris.append((roledefinition, linkroleUri))
    linkroleUris.sort()
linkroleUris

実行すると、損益計算書や貸借対照表などが含まれていることがわかります。

ですので前回取得したデータの中から貸借対照表に関するデータのみ取り出してみましょう。

df[df['LinkDefinition']=='310040 貸借対照表']

このようにすることで、貸借対照表関連のデータのみを抽出できます。

財務データの抽出

有価証券報告書全体には財務諸表の数値データ以外にも、事業に関する情報や、セグメント情報、役員に関する情報など様々なデータが含まれています。数値データだけではなく文章のデータも多くあります。先ほどのデータフレームの状態では様々なデータが入り混じった状態になっているので、うまく整理をして財務諸表に関するデータのみを抽出したいと考えています。
ただし同じ財務諸表のデータでも、いつの期間の情報なのかといったことや、連結のデータなのか個別のデータなのかといった要素が異なってきます。
それらをうまく区別できるように整理をして最終的に財務諸表のデータを表として出力する関数を作成しました。

def same_name(df):
    if len(df) == 2:
        if ('期首' in df.iloc[0,:]['Label'] and '期末' in df.iloc[1,:]['Label']) or ('期末' in df.iloc[0,:]['Label'] and '期首' in df.iloc[1,:]['Label']):
            return df[df['Label'].str.contains('期末')]
    else:
        df['StandardLabel'] = df['Label']
        if len(df[df.duplicated(subset=['StandardLabel'])]):
            print('ラベル重複')
            return None
def clean_and_convert(value):
    numeric_pattern = r'^[-+]?\d+(,\d{3})*(\.\d+)?$'


    str_value = str(value)  # 値を文字列に変換
    if re.match(numeric_pattern, str_value):  # 数値のパターンにマッチする場合
        cleaned_value = str_value.replace(',', '')  # カンマを削除
        return float(cleaned_value)
    else:
        return np.nan
# 非欠損値の割合を計算する関数
def non_null_ratio(column):
    non_null_count = column.count()  # 非欠損値の数をカウント
    total_count = len(column)  # 列の総要素数
    return non_null_count / total_count


def parse_xbrl(xbrl_file):
    ctrl = Cntlr.Cntlr(logFileName='logToPrint')
    modelXbrl = ctrl.modelManager.load(xbrl_file)   
    ViewFileFactTable.viewFacts(modelXbrl,'fact.csv', lang='ja',cols=cols)
    df = pd.read_csv('fact.csv')
    link_keywords = ['310040 貸借対照表','321040 損益計算書','342040 キャッシュ・フロー計算書','330040 株主資本等変動計算書']
    df['LinkDefinition'] = df['LinkDefinition'].fillna('')
    df_ex = df[df['LinkDefinition'].str.startswith('3')]
    df_ex = df_ex[df_ex['Type'].astype(str)=='xbrli:monetaryItemType']
    df_ex = df_ex.dropna(axis=1,how='all')
    pattern = r'\d{4}-\d{2}-\d{2} - (非連結又は個別)'
    no_cols = [col for col in df_ex.columns if isinstance(col, str) and re.match(pattern, col)]


    co_cols = []
    for no_col in no_cols:
        co_col = no_col.split(' - ')[0]

        if co_col in df_ex.columns:

            co_cols.append(co_col)

    df_ex_co = df_ex.loc[:,co_cols+[i for i in cols if i in df_ex.columns]]
    df_ex_no = df_ex.loc[:,no_cols+[i for i in cols if i in df_ex.columns]]
    # df_ex.to_csv('df_ex.csv')




    def process(df_ex, co_cols):


        set_cols = ['StandardLabel','Label'] + co_cols
        concept_cols = [i for i in df_ex.columns if i not in cols]
        df_ex = df_ex.drop_duplicates(subset=set_cols).copy()
        for co_col in co_cols:
            df_ex[co_col] = df_ex[co_col].replace('',np.nan)
        df_ex['StandardLabel_original'] = df_ex['StandardLabel']
        duplicates = df_ex[df_ex.duplicated(subset=['StandardLabel'], keep=False)]
        no_duplicates = df_ex[~(df_ex.duplicated(subset=['StandardLabel'], keep=False))]


        labels = list(duplicates['StandardLabel_original'].unique())
        labelnames = dict(zip(df_ex['LocalName'], df_ex['StandardLabel']))
        duplicates_new = pd.DataFrame()
        for label in labels:
            dup_label = duplicates[duplicates['StandardLabel'] == label]
            dup_lname = dup_label[dup_label.duplicated(subset=['LocalName','ParentName'], keep=False)].copy()
            dup_slabel = dup_label[~(dup_label.duplicated(subset=['LocalName','ParentName'], keep=False))].copy()


            if len(dup_slabel) > 0:
                dup_slabel['addLabel'] = dup_slabel['ParentLocalName'].str.replace('Abstract','').map(labelnames).fillna(dup_slabel['LinkDefinition'])
                dup_slabel['StandardLabel'] = dup_slabel['addLabel'] +'-'+ dup_slabel['StandardLabel'] 
            else:
                dup_slabel = pd.DataFrame(columns=no_duplicates.columns)
            dup_lname_new = pd.DataFrame(columns=no_duplicates.columns)
            if len(dup_lname) > 0:
                grouped = dup_lname.groupby(['ParentName','LocalName'])
                group_combinations = grouped.groups.keys()

                for group_combination in group_combinations:
                    group_df = grouped.get_group(group_combination)
                    add_data = same_name(group_df)
                    dup_lname_new = pd.concat([dup_lname_new, add_data])
            duplicates_new = pd.concat([duplicates_new,dup_slabel.loc[:,no_duplicates.columns], dup_lname_new.loc[:,no_duplicates.columns]])
        assert len(duplicates_new[duplicates_new.duplicated(subset=['StandardLabel'], keep=False)]) == 0
        df_result = pd.concat([no_duplicates, duplicates_new])       


        assert len(df_result[df_result.duplicated(subset=['StandardLabel'], keep=False)]) == 0
        threshold = 0.6






        # 閾値以下の列を取り出す
        df_result.loc[:,concept_cols] = df_result.loc[:,concept_cols].replace('',np.nan)
        selected_columns = [col for col in concept_cols if non_null_ratio(df_result[col]) >= threshold]


        # 選択された列を表示
        data = df_result[selected_columns+['StandardLabel']].sort_index().set_index('StandardLabel',drop=True)
        # 正規表現パターンの定義(数値のパターン)
        numeric_pattern = r'^[-+]?\d+(,\d{3})*(\.\d+)?$'






        # 各セルの値をクリーニングして数値に変換
        for col in data.columns:
            data[col] = data[col].apply(clean_and_convert)
        return data
    co_result = process(df_ex_co, co_cols)
    no_result = process(df_ex_no, no_cols)
    no_result.columns = [i.replace(' - 非連結又は個別','') for i in no_result.columns]
    no_result.index = no_result.index + '_個別' 
    # no_result.columns = [i+'_個別' for i in no_result.columns if i in concept_cols]
    df_result = pd.concat([co_result,no_result]).dropna()
    #
    return df_result.T
parse_xbrl(xbrl_file)

この関数を実行すると以下の画像のように財務データの表を出力することができます

結果

コードの解説

1. same_name 関数

def same_name(df):
    if len(df) == 2:
        if ('期首' in df.iloc[0,:]['Label'] and '期末' in df.iloc[1,:]['Label']) or ('期末' in df.iloc[0,:]['Label'] and '期首' in df.iloc[1,:]['Label']):
            return df[df['Label'].str.contains('期末')]
    else:
        df['StandardLabel'] = df['Label']
        if len(df[df.duplicated(subset=['StandardLabel'])]):
            print('ラベル重複')
            return None

この関数は、データフレーム df のラベルをチェックし、特定の条件に基づいてフィルタリングします。
– データフレームの行数が2行の場合、特定のラベル(期首期末)の組み合わせがあるか確認し、期末 を含む行を返します。
– それ以外の場合、StandardLabel を設定し、重複があればメッセージを表示して None を返します。

2. clean_and_convert 関数

def clean_and_convert(value):
    numeric_pattern = r'^[-+]?\d+(,\d{3})*(\.\d+)?$'

    str_value = str(value)  # 値を文字列に変換
    if re.match(numeric_pattern, str_value):  # 数値のパターンにマッチする場合
        cleaned_value = str_value.replace(',', '')  # カンマを削除
        return float(cleaned_value)
    else:
        return np.nan

この関数は、入力値をクリーンアップし、数値に変換します。
– 入力値を文字列に変換し、数値のパターンにマッチするかチェックします。
– 数値のパターンにマッチする場合、カンマを削除して数値に変換し、返します。
– マッチしない場合、NaN を返します。

3. non_null_ratio 関数

def non_null_ratio(column):
    non_null_count = column.count()  # 非欠損値の数をカウント
    total_count = len(column)  # 列の総要素数
    return non_null_count / total_count

この関数は、指定された列の非欠損値の割合を計算します。
– 非欠損値の数をカウントし、全体の要素数で割って割合を計算します。

4. parse_xbrl 関数

def parse_xbrl(xbrl_file):
    ctrl = Cntlr.Cntlr(logFileName='logToPrint')
    modelXbrl = ctrl.modelManager.load(xbrl_file)
    ViewFileFactTable.viewFacts(modelXbrl,'fact.csv', lang='ja',cols=cols)
    df = pd.read_csv('fact.csv')
    ...

この関数は、XBRLファイルをパースして処理します。
Cntlr オブジェクトを作成し、XBRLファイルを読み込みます。
– 事実データを fact.csv ファイルに書き出し、それを読み込みます。

    link_keywords = ['310040 貸借対照表','321040 損益計算書','342040 キャッシュ・フロー計算書','330040 株主資本等変動計算書']
    df['LinkDefinition'] = df['LinkDefinition'].fillna('')
    df_ex = df[df['LinkDefinition'].str.startswith('3')]
    df_ex = df_ex[df_ex['Type'].astype(str)=='xbrli:monetaryItemType']
    df_ex = df_ex.dropna(axis=1,how='all')
    pattern = r'\d{4}-\d{2}-\d{2} - (非連結又は個別)'
    no_cols = [col for col in df_ex.columns if isinstance(col, str) and re.match(pattern, col)]
  • 特定のキーワードに基づいてフィルタリングを行い、LinkDefinition が ‘3’ で始まる行を抽出します。
  • ‘xbrli:monetaryItemType’ タイプのデータを抽出し、すべての欠損列を削除します。
  • 特定のパターンにマッチする列を抽出します。

5. process 関数

    def process(df_ex, co_cols):
        set_cols = ['StandardLabel','Label'] + co_cols
        concept_cols = [i for i in df_ex.columns if i not in cols]
        df_ex = df_ex.drop_duplicates(subset=set_cols).copy()
        ...
        return data

この関数は、データフレームをクリーンアップし、特定の処理を行います。
set_colsconcept_cols を設定し、重複を削除します。
– 列の値をクリーンアップし、重複を処理し、新しいデータフレームを返します。

6. 結果の結合と返却

    co_result = process(df_ex_co, co_cols)
    no_result = process(df_ex_no, no_cols)
    no_result.columns = [i.replace(' - 非連結又は個別','') for i in no_result.columns]
    no_result.index = no_result.index + '_個別'
    df_result = pd.concat([co_result,no_result]).dropna()
    return df_result.T
  • process 関数を使って、個別と連結のデータを処理します。
  • 結果を結合し、欠損値を削除して転置し、最終的なデータフレームを返します。
タイトルとURLをコピーしました