<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>아미(아름다운미소)</title>
    <link>https://ljj777.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 30 May 2026 01:43:41 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>유키공</managingEditor>
    <image>
      <title>아미(아름다운미소)</title>
      <url>https://tistory1.daumcdn.net/tistory/2853342/attach/aed9f62d7e204649abcbdcf76d785d83</url>
      <link>https://ljj777.tistory.com</link>
    </image>
    <item>
      <title>암호화엑셀을 CSV로 변경</title>
      <link>https://ljj777.tistory.com/1111</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;import win32com.client as win32&lt;br&gt;import os&lt;br&gt;&lt;br&gt;excel = win32.Dispatch(&quot;Excel.Application&quot;)&lt;br&gt;excel.Visible = False&lt;br&gt;excel.DisplayAlerts = False&lt;br&gt;&lt;br&gt;folder = r&quot;C:\excel_folder&quot;&lt;br&gt;&lt;br&gt;try:&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for file in os.listdir(folder):&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if file.endswith(&quot;.xlsx&quot;):&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;xlsx = os.path.join(folder, file)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;wb = excel.Workbooks.Open(xlsx)&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for sheet in wb.Worksheets:&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;csv = os.path.join(&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;folder,&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;f&quot;{file.replace('.xlsx','')}_{sheet.Name}.csv&quot;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sheet.SaveAs(csv, FileFormat=62)&lt;br&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;wb.Close(False)&lt;br&gt;&lt;br&gt;finally:&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;excel.Quit()&amp;nbsp;&amp;nbsp; # ⭐ 에러 나도 엑셀 강제 종료&lt;/p&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1111</guid>
      <comments>https://ljj777.tistory.com/1111#entry1111comment</comments>
      <pubDate>Wed, 29 Apr 2026 11:57:15 +0900</pubDate>
    </item>
    <item>
      <title>한국 &amp;amp; 영국 상담 시간 판별 로직</title>
      <link>https://ljj777.tistory.com/1110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;한국 &amp;amp; 영국 상담 시간 판별 로직&lt;br&gt;​보통 상담 시간을 09:00 ~ 18:00라고 가정했을 때의 코드입니다.&lt;br&gt;&lt;br&gt;import pandas as pd&lt;br&gt;&lt;br&gt;# 1. UTC 기반의 원본 데이터를 읽고 변환 (한 방 코드)&lt;br&gt;df['time_utc'] = pd.to_datetime(df['time'], utc=True, errors='coerce')&lt;br&gt;&lt;br&gt;# 2. 한국 로컬 시간 생성 및 상담 여부 판별&lt;br&gt;df['time_kr'] = df['time_utc'].dt.tz_convert('Asia/Seoul')&lt;br&gt;df['is_kr_biz_hours'] = df['time_kr'].dt.hour.between(9, 17) # 09:00 ~ 17:59&lt;br&gt;&lt;br&gt;# 3. 영국 로컬 시간 생성 및 상담 여부 판별 (서머타임 자동 계산)&lt;br&gt;df['time_uk'] = df['time_utc'].dt.tz_convert('Europe/London')&lt;br&gt;df['is_uk_biz_hours'] = df['time_uk'].dt.hour.between(9, 17)&lt;br&gt;&lt;br&gt;&lt;/p&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1110</guid>
      <comments>https://ljj777.tistory.com/1110#entry1110comment</comments>
      <pubDate>Sat, 11 Apr 2026 21:58:36 +0900</pubDate>
    </item>
    <item>
      <title>Json</title>
      <link>https://ljj777.tistory.com/1104</link>
      <description>&lt;pre id=&quot;code_1759108216184&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import requests
import pandas as pd

urls = [
    &quot;https://jsonplaceholder.typicode.com/todos/1&quot;,
    &quot;https://jsonplaceholder.typicode.com/todos/2&quot;,
    &quot;https://jsonplaceholder.typicode.com/todos/3&quot;
]

results = []

for url in urls:
    resp = requests.get(url)
    if resp.status_code == 200:
        data = resp.json()       # JSON &amp;rarr; dict
        results.append(data)     # dict 누적

# dict 리스트 &amp;rarr; DataFrame
df = pd.DataFrame(results)

print(df.head())&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1104</guid>
      <comments>https://ljj777.tistory.com/1104#entry1104comment</comments>
      <pubDate>Mon, 29 Sep 2025 10:10:28 +0900</pubDate>
    </item>
    <item>
      <title>시간차이</title>
      <link>https://ljj777.tistory.com/1103</link>
      <description>&lt;pre id=&quot;code_1756193350962&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from datetime import datetime

# 문자열 형태의 시간
time_str1 = &quot;2023-10-15 14:30:00&quot;
time_str2 = &quot;2023-10-15 16:45:30&quot;

# 문자열을 datetime 객체로 변환
time1 = datetime.strptime(time_str1, &quot;%Y-%m-%d %H:%M:%S&quot;)
time2 = datetime.strptime(time_str2, &quot;%Y-%m-%d %H:%M:%S&quot;)

# 차이 계산
difference = time2 - time1
print(f&quot;시간 차이: {difference}&quot;)&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1103</guid>
      <comments>https://ljj777.tistory.com/1103#entry1103comment</comments>
      <pubDate>Tue, 26 Aug 2025 16:29:22 +0900</pubDate>
    </item>
    <item>
      <title>2주운동 식단플랜</title>
      <link>https://ljj777.tistory.com/1102</link>
      <description>&lt;pre id=&quot;code_1755148695872&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from openpyxl import Workbook
from openpyxl.styles import Alignment, Font

# 새 워크북 생성
wb = Workbook()

# 운동 시트
ws = wb.active
ws.title = &quot;운동 플랜&quot;

# 열 너비 조정
columns = ['A', 'B', 'C', 'D', 'E']
for col in columns:
    ws.column_dimensions[col].width = 25

# 헤더
headers = [&quot;구분&quot;, &quot;운동&quot;, &quot;횟수/시간&quot;, &quot;세트&quot;, &quot;비고&quot;]
ws.append(headers)
for cell in ws[1]:
    cell.alignment = Alignment(horizontal='center', vertical='center')
    cell.font = Font(bold=True)

# 운동 데이터 (그림 없이)
workouts = [
    (&quot;유산소&quot;, &quot;빠른 걷기 (트레드밀)&quot;, &quot;20분(1주차) &amp;rarr; 30분(2주차)&quot;, &quot;-&quot;, &quot;속도 6.0~6.5km/h&quot;),
    (&quot;근력&quot;, &quot;레그프레스&quot;, &quot;10~12회&quot;, &quot;3세트&quot;, &quot;무릎&amp;middot;허리 부담 &amp;darr;&quot;),
    (&quot;근력&quot;, &quot;체스트 프레스&quot;, &quot;8~10회&quot;, &quot;3세트&quot;, &quot;가슴&amp;middot;팔 근육&quot;),
    (&quot;근력&quot;, &quot;시티드 로우 머신&quot;, &quot;10~12회&quot;, &quot;3세트&quot;, &quot;등 근육&quot;),
    (&quot;근력&quot;, &quot;플랭크&quot;, &quot;10~15초 유지&quot;, &quot;3세트&quot;, &quot;복부&amp;middot;코어 강화&quot;),
]

for w in workouts:
    ws.append(w)

# 식단 시트
ws_diet = wb.create_sheet(title=&quot;식단&quot;)
diet = [
    &quot;아침: 삶은 달걀 2개 + 채소 + 현미밥 소량&quot;,
    &quot;점심: 일반식(밥은 평소보다 70%) + 단백질 반찬 위주&quot;,
    &quot;간식: 플레인 요거트, 견과류 한 줌, 프로틴 쉐이크 중 택1&quot;,
    &quot;저녁: 샐러드 + 단백질(닭가슴살/두부/생선), 밥은 소량 또는 생략&quot;,
    &quot;음료: 물 2L, 단 음료&amp;middot;술&amp;middot;과자 최소화&quot;
]
for i, item in enumerate(diet, start=1):
    ws_diet[f&quot;A{i}&quot;] = item

# 파일 저장
file_path = &quot;2주_운동_식단_플랜.xlsx&quot;
wb.save(file_path)
print(f&quot;엑셀 파일 생성 완료: {file_path}&quot;)&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1102</guid>
      <comments>https://ljj777.tistory.com/1102#entry1102comment</comments>
      <pubDate>Thu, 14 Aug 2025 14:18:46 +0900</pubDate>
    </item>
    <item>
      <title>재무제표 최종</title>
      <link>https://ljj777.tistory.com/1101</link>
      <description>&lt;pre id=&quot;code_1754523870290&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd
import requests
from typing import Optional, Dict, Any, Tuple
import warnings
warnings.filterwarnings('ignore')

class EnhancedFinancialAnalyzer:
    &quot;&quot;&quot;네이버 금융 재무제표 분석기 - 확장된 재무비율 포함&quot;&quot;&quot;

    def __init__(self):
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }

    def get_financial_statement(self, ticker: str) -&amp;gt; Optional[pd.DataFrame]:
        &quot;&quot;&quot;네이버 금융에서 손익계산서 데이터를 가져오는 함수&quot;&quot;&quot;
        url = f'https://finance.naver.com/item/main.naver?code={ticker}'

        try:
            response = requests.get(url, headers=self.headers, timeout=10)
            if response.status_code != 200:
                print(f&quot;⚠️ 페이지 접근 실패: HTTP {response.status_code}&quot;)
                return None

            tables = pd.read_html(url, encoding='euc-kr', header=0)

        except requests.exceptions.RequestException as e:
            print(f&quot;⚠️ 네트워크 오류: {e}&quot;)
            return None
        except Exception as e:
            print(f&quot;⚠️ 데이터 파싱 오류: {e}&quot;)
            return None

        # 손익계산서 테이블 찾기
        for i, table in enumerate(tables):
            if table.shape[1] &amp;gt;= 3 and len(table) &amp;gt; 5:
                first_col = table.iloc[:, 0].astype(str).str.strip()
                if any('매출' in cell for cell in first_col):
                    print(f&quot;✅ 재무제표 발견 (테이블 #{i+1})&quot;)
                    return table

        print(&quot;⚠️ 손익계산서 테이블을 찾을 수 없습니다&quot;)
        return None

    def get_balance_sheet(self, ticker: str) -&amp;gt; Optional[pd.DataFrame]:
        &quot;&quot;&quot;네이버 금융에서 재무상태표 데이터를 가져오는 함수&quot;&quot;&quot;
        url = f'https://finance.naver.com/item/main.naver?code={ticker}'

        try:
            response = requests.get(url, headers=self.headers, timeout=10)
            if response.status_code != 200:
                return None

            tables = pd.read_html(url, encoding='euc-kr', header=0)

            # 재무상태표 테이블 찾기 (자산, 부채 등이 포함된 테이블)
            for i, table in enumerate(tables):
                if table.shape[1] &amp;gt;= 3 and len(table) &amp;gt; 5:
                    first_col = table.iloc[:, 0].astype(str).str.strip()
                    if any(keyword in cell for keyword in ['자산', '부채', '자본'] for cell in first_col):
                        print(f&quot;✅ 재무상태표 발견 (테이블 #{i+1})&quot;)
                        return table

        except Exception as e:
            print(f&quot;⚠️ 재무상태표 데이터 오류: {e}&quot;)

        return None

    def get_company_info(self, ticker: str) -&amp;gt; Dict[str, Any]:
        &quot;&quot;&quot;기업 기본정보 및 주가 정보 수집&quot;&quot;&quot;
        try:
            # 주식 정보 페이지에서 시가총액, 주가 등 정보 수집
            url = f'https://finance.naver.com/item/main.naver?code={ticker}'
            response = requests.get(url, headers=self.headers, timeout=10)

            if response.status_code != 200:
                return {}

            tables = pd.read_html(url, encoding='euc-kr')

            # 시가총액, 주가 등 정보가 있는 테이블 찾기
            company_info = {}

            for table in tables:
                if len(table.columns) &amp;gt;= 2:
                    # 테이블을 문자열로 변환하여 검색
                    table_str = table.astype(str)
                    if table_str.apply(lambda x: x.str.contains('시가총액|주가|거래량', na=False)).any().any():
                        # 시가총액 정보 추출 시도
                        try:
                            for idx, row in table.iterrows():
                                if '시가총액' in str(row.iloc[0]):
                                    company_info['시가총액'] = str(row.iloc[1])
                                elif '현재가' in str(row.iloc[0]) or '주가' in str(row.iloc[0]):
                                    company_info['현재가'] = str(row.iloc[1])
                        except:
                            continue

            return company_info

        except Exception as e:
            print(f&quot;⚠️ 기업정보 수집 오류: {e}&quot;)
            return {}

    def clean_financial_df(self, df: pd.DataFrame) -&amp;gt; pd.DataFrame:
        &quot;&quot;&quot;데이터프레임 전처리&quot;&quot;&quot;
        df_copy = df.copy()
        df_copy.set_index(df_copy.columns[0], inplace=True)
        df_copy = df_copy.replace(['-', '/', 'N/A', '', ' '], '0')

        def convert_to_number(x):
            if pd.isna(x) or x == '':
                return 0
            try:
                if isinstance(x, str):
                    cleaned = x.replace(',', '').replace('(', '-').replace(')', '').strip()
                    return float(cleaned) if cleaned else 0
                return float(x)
            except (ValueError, TypeError):
                return 0

        for col in df_copy.columns:
            df_copy[col] = df_copy[col].apply(convert_to_number)

        return df_copy

    def find_row_name(self, df: pd.DataFrame, candidates: list) -&amp;gt; str:
        &quot;&quot;&quot;유연한 행 이름 매칭&quot;&quot;&quot;
        index_str = df.index.astype(str).str.strip()

        for candidate in candidates:
            if candidate in index_str.values:
                return candidate

            matches = index_str[index_str.str.contains(candidate, na=False)]
            if len(matches) &amp;gt; 0:
                return matches.iloc[0]

        raise KeyError(f&quot;다음 항목을 찾을 수 없습니다: {candidates}&quot;)

    def calculate_growth_rate(self, current: float, previous: float) -&amp;gt; Tuple[str, float]:
        &quot;&quot;&quot;성장률 계산 (문자열과 숫자값 모두 반환)&quot;&quot;&quot;
        try:
            if previous == 0:
                return &quot;N/A (이전값 0)&quot;, 0

            growth_rate = ((current - previous) / abs(previous)) * 100
            return f&quot;{growth_rate:.2f}%&quot;, growth_rate

        except (ZeroDivisionError, TypeError):
            return &quot;N/A&quot;, 0

    def calculate_extended_ratios(self, income_df: pd.DataFrame, balance_df: Optional[pd.DataFrame] = None) -&amp;gt; Dict[str, Any]:
        &quot;&quot;&quot;확장된 재무비율 계산&quot;&quot;&quot;
        ratios = {}

        try:
            income_cleaned = self.clean_financial_df(income_df)

            # 최신 두 기간 데이터
            if len(income_cleaned.columns) &amp;lt; 2:
                return {'오류': '비교할 데이터가 충분하지 않습니다'}

            latest = income_cleaned.columns[-1]
            prev = income_cleaned.columns[-2]

            # 손익계산서 주요 항목
            revenue_current = income_cleaned.loc[self.find_row_name(income_cleaned, ['매출액', '수익(매출액)', '총매출액']), latest]
            revenue_previous = income_cleaned.loc[self.find_row_name(income_cleaned, ['매출액', '수익(매출액)', '총매출액']), prev]

            operating_current = income_cleaned.loc[self.find_row_name(income_cleaned, ['영업이익', '영업이익(손실)', '영업손익']), latest]
            operating_previous = income_cleaned.loc[self.find_row_name(income_cleaned, ['영업이익', '영업이익(손실)', '영업손익']), prev]

            net_current = income_cleaned.loc[self.find_row_name(income_cleaned, ['당기순이익', '당기순이익(손실)', '순이익', '당기순손익']), latest]
            net_previous = income_cleaned.loc[self.find_row_name(income_cleaned, ['당기순이익', '당기순이익(손실)', '순이익', '당기순손익']), prev]

            # 1. 수익성 비율 (Profitability Ratios)
            if revenue_current != 0:
                ratios['매출총이익률'] = f&quot;{((revenue_current - 0) / revenue_current * 100):.2f}%&quot;  # 매출원가 데이터 필요시 수정
                ratios['영업이익률'] = f&quot;{(operating_current / revenue_current * 100):.2f}%&quot;
                ratios['순이익률'] = f&quot;{(net_current / revenue_current * 100):.2f}%&quot;

                # EBITDA 추정 (감가상각비 데이터가 있다면 더 정확)
                try:
                    # 감가상각비 찾기 시도
                    depreciation = 0
                    try:
                        depreciation_row = self.find_row_name(income_cleaned, ['감가상각비', '상각비'])
                        depreciation = income_cleaned.loc[depreciation_row, latest]
                    except KeyError:
                        # 감가상각비를 찾을 수 없으면 영업이익의 10%로 추정
                        depreciation = operating_current * 0.1

                    ebitda = operating_current + depreciation
                    ratios['EBITDA'] = f&quot;{ebitda:,.0f}백만원&quot;
                    ratios['EBITDA마진'] = f&quot;{(ebitda / revenue_current * 100):.2f}%&quot; if revenue_current != 0 else &quot;N/A&quot;
                except:
                    ratios['EBITDA'] = &quot;계산불가&quot;
                    ratios['EBITDA마진'] = &quot;계산불가&quot;

            # 2. 성장성 비율 (Growth Ratios)
            revenue_growth_str, revenue_growth_val = self.calculate_growth_rate(revenue_current, revenue_previous)
            operating_growth_str, operating_growth_val = self.calculate_growth_rate(operating_current, operating_previous)
            net_growth_str, net_growth_val = self.calculate_growth_rate(net_current, net_previous)

            ratios['매출액증가율'] = revenue_growth_str
            ratios['영업이익증가율'] = operating_growth_str
            ratios['순이익증가율'] = net_growth_str

            # 재무상태표 기반 비율 (데이터가 있는 경우)
            if balance_df is not None:
                try:
                    balance_cleaned = self.clean_financial_df(balance_df)

                    # 자산 관련
                    total_assets = balance_cleaned.loc[self.find_row_name(balance_cleaned, ['자산총계', '총자산', '자산합계']), latest]

                    # 부채 관련
                    total_liabilities = balance_cleaned.loc[self.find_row_name(balance_cleaned, ['부채총계', '총부채', '부채합계']), latest]

                    # 자본 관련
                    total_equity = balance_cleaned.loc[self.find_row_name(balance_cleaned, ['자본총계', '총자본', '자본합계', '자기자본']), latest]

                    # 3. 안전성 비율 (Stability Ratios)
                    if total_assets != 0:
                        ratios['부채비율'] = f&quot;{(total_liabilities / total_equity * 100):.2f}%&quot; if total_equity != 0 else &quot;N/A&quot;
                        ratios['자기자본비율'] = f&quot;{(total_equity / total_assets * 100):.2f}%&quot;
                        ratios['부채자산비율'] = f&quot;{(total_liabilities / total_assets * 100):.2f}%&quot;

                    # 4. 활동성 비율 (Activity Ratios)
                    if total_assets != 0:
                        ratios['총자산회전율'] = f&quot;{(revenue_current / total_assets):.2f}회&quot;

                    # 5. 수익성 심화 분석
                    if total_assets != 0:
                        ratios['ROA(총자산수익률)'] = f&quot;{(net_current / total_assets * 100):.2f}%&quot;
                    if total_equity != 0:
                        ratios['ROE(자기자본수익률)'] = f&quot;{(net_current / total_equity * 100):.2f}%&quot;

                except KeyError as e:
                    ratios['재무상태표_오류'] = f&quot;재무상태표 항목 부족: {str(e)}&quot;
                except Exception as e:
                    ratios['재무상태표_계산오류'] = str(e)

            # 6. 종합 평가 점수 시스템
            score = 0
            max_score = 0

            # 수익성 점수 (40점)
            max_score += 40
            if revenue_current &amp;gt; 0:
                operating_margin = operating_current / revenue_current * 100
                if operating_margin &amp;gt;= 20: score += 15
                elif operating_margin &amp;gt;= 15: score += 12
                elif operating_margin &amp;gt;= 10: score += 8
                elif operating_margin &amp;gt;= 5: score += 4
                elif operating_margin &amp;gt;= 0: score += 1

                net_margin = net_current / revenue_current * 100
                if net_margin &amp;gt;= 15: score += 15
                elif net_margin &amp;gt;= 10: score += 12
                elif net_margin &amp;gt;= 5: score += 8
                elif net_margin &amp;gt;= 2: score += 4
                elif net_margin &amp;gt;= 0: score += 1

                # EBITDA 마진 평가
                try:
                    ebitda_margin = float(ratios.get('EBITDA마진', '0%').replace('%', ''))
                    if ebitda_margin &amp;gt;= 25: score += 10
                    elif ebitda_margin &amp;gt;= 20: score += 8
                    elif ebitda_margin &amp;gt;= 15: score += 6
                    elif ebitda_margin &amp;gt;= 10: score += 3
                    elif ebitda_margin &amp;gt;= 5: score += 1
                except:
                    pass

            # 성장성 점수 (30점)
            max_score += 30
            if revenue_growth_val &amp;gt;= 20: score += 10
            elif revenue_growth_val &amp;gt;= 10: score += 8
            elif revenue_growth_val &amp;gt;= 5: score += 6
            elif revenue_growth_val &amp;gt;= 0: score += 3

            if operating_growth_val &amp;gt;= 30: score += 10
            elif operating_growth_val &amp;gt;= 15: score += 8
            elif operating_growth_val &amp;gt;= 5: score += 6
            elif operating_growth_val &amp;gt;= 0: score += 3

            if net_growth_val &amp;gt;= 30: score += 10
            elif net_growth_val &amp;gt;= 15: score += 8
            elif net_growth_val &amp;gt;= 5: score += 6
            elif net_growth_val &amp;gt;= 0: score += 3

            # 안정성 점수 (30점) - 재무상태표 데이터가 있는 경우만
            if balance_df is not None and '부채비율' in ratios:
                max_score += 30
                try:
                    debt_ratio = float(ratios['부채비율'].replace('%', ''))
                    if debt_ratio &amp;lt;= 30: score += 15
                    elif debt_ratio &amp;lt;= 50: score += 12
                    elif debt_ratio &amp;lt;= 100: score += 8
                    elif debt_ratio &amp;lt;= 200: score += 4
                    elif debt_ratio &amp;lt;= 300: score += 1

                    equity_ratio = float(ratios['자기자본비율'].replace('%', ''))
                    if equity_ratio &amp;gt;= 70: score += 15
                    elif equity_ratio &amp;gt;= 50: score += 12
                    elif equity_ratio &amp;gt;= 30: score += 8
                    elif equity_ratio &amp;gt;= 20: score += 4
                    elif equity_ratio &amp;gt;= 10: score += 1
                except:
                    max_score -= 30

            # 최종 점수 계산
            if max_score &amp;gt; 0:
                final_score = (score / max_score) * 100
                ratios['종합점수'] = f&quot;{final_score:.1f}점 ({score}/{max_score})&quot;

                if final_score &amp;gt;= 80:
                    ratios['투자등급'] = &quot;  우수 (A급)&quot;
                elif final_score &amp;gt;= 65:
                    ratios['투자등급'] = &quot;  양호 (B급)&quot;
                elif final_score &amp;gt;= 50:
                    ratios['투자등급'] = &quot;  보통 (C급)&quot;
                elif final_score &amp;gt;= 35:
                    ratios['투자등급'] = &quot;  주의 (D급)&quot;
                else:
                    ratios['투자등급'] = &quot;  위험 (E급)&quot;
            else:
                ratios['종합점수'] = &quot;계산불가&quot;
                ratios['투자등급'] = &quot;평가불가&quot;

        except Exception as e:
            ratios['계산오류'] = str(e)

        return ratios

    def analyze_financials_extended(self, ticker: str) -&amp;gt; Dict[str, Any]:
        &quot;&quot;&quot;확장된 재무 분석&quot;&quot;&quot;
        print(f&quot;\n{'='*60}&quot;)
        print(f&quot;  [{ticker}] 심화 재무제표 분석 보고서&quot;)
        print(f&quot;{'='*60}&quot;)

        # 손익계산서 데이터 수집
        income_df = self.get_financial_statement(ticker)
        if income_df is None:
            return {'오류': '손익계산서 데이터를 가져올 수 없습니다'}

        # 재무상태표 데이터 수집 (선택적)
        balance_df = self.get_balance_sheet(ticker)
        if balance_df is not None:
            print(&quot;✅ 재무상태표 데이터도 확보됨 - 더 정확한 분석 가능&quot;)
        else:
            print(&quot;⚠️ 재무상태표 데이터 없음 - 손익계산서 중심 분석&quot;)

        # 기업 정보 수집
        company_info = self.get_company_info(ticker)

        # 확장된 재무비율 계산
        ratios = self.calculate_extended_ratios(income_df, balance_df)

        # 기본 정보 출력
        print(f&quot;\n  기업 기본정보:&quot;)
        print(&quot;-&quot; * 30)
        for key, value in company_info.items():
            print(f&quot;{key}: {value}&quot;)

        # 재무비율 출력
        print(f&quot;\n  재무비율 분석 결과:&quot;)
        print(&quot;-&quot; * 30)

        # 카테고리별로 구분하여 출력
        categories = {
            '  수익성 지표': ['매출총이익률', '영업이익률', '순이익률', 'EBITDA', 'EBITDA마진', 'ROA(총자산수익률)', 'ROE(자기자본수익률)'],
            '  성장성 지표': ['매출액증가율', '영업이익증가율', '순이익증가율'],
            ' ️ 안정성 지표': ['부채비율', '자기자본비율', '부채자산비율'],
            '  활동성 지표': ['총자산회전율'],
            '⭐ 종합평가': ['종합점수', '투자등급']
        }

        for category, metrics in categories.items():
            category_ratios = {k: v for k, v in ratios.items() if k in metrics}
            if category_ratios:
                print(f&quot;\n{category}:&quot;)
                for metric, value in category_ratios.items():
                    print(f&quot;  &amp;bull; {metric}: {value}&quot;)

        # 기타 정보 출력
        other_ratios = {k: v for k, v in ratios.items()
                       if k not in sum(categories.values(), [])
                       and not k.endswith('오류')}

        if other_ratios:
            print(f&quot;\n  추가 정보:&quot;)
            for key, value in other_ratios.items():
                print(f&quot;  &amp;bull; {key}: {value}&quot;)

        # 오류 정보 출력
        error_ratios = {k: v for k, v in ratios.items() if k.endswith('오류')}
        if error_ratios:
            print(f&quot;\n⚠️ 분석 제한사항:&quot;)
            for key, value in error_ratios.items():
                print(f&quot;  &amp;bull; {key}: {value}&quot;)

        print(f&quot;\n{'='*60}&quot;)
        return ratios

    def compare_companies(self, tickers: list) -&amp;gt; pd.DataFrame:
        &quot;&quot;&quot;여러 기업 재무비율 비교&quot;&quot;&quot;
        print(f&quot;\n{'='*70}&quot;)
        print(f&quot;  기업 비교 분석 ({len(tickers)}개 기업)&quot;)
        print(f&quot;{'='*70}&quot;)

        comparison_data = []

        for ticker in tickers:
            print(f&quot;\n  {ticker} 분석 중...&quot;)

            income_df = self.get_financial_statement(ticker)
            if income_df is None:
                continue

            balance_df = self.get_balance_sheet(ticker)
            ratios = self.calculate_extended_ratios(income_df, balance_df)

            # 비교용 데이터 추출
            company_data = {'종목코드': ticker}

            # 주요 지표만 선택
            key_metrics = ['영업이익률', '순이익률', 'ROE(자기자본수익률)', 'ROA(총자산수익률)',
                          '매출액증가율', '영업이익증가율', '부채비율', '자기자본비율', '종합점수', '투자등급']

            for metric in key_metrics:
                company_data[metric] = ratios.get(metric, 'N/A')

            comparison_data.append(company_data)

        # 데이터프레임 생성
        if comparison_data:
            comparison_df = pd.DataFrame(comparison_data)
            print(f&quot;\n  비교 결과:&quot;)
            print(&quot;-&quot; * 70)
            print(comparison_df.to_string(index=False))
            return comparison_df
        else:
            print(&quot;❌ 비교할 데이터가 없습니다&quot;)
            return pd.DataFrame()


def main():
    analyzer = EnhancedFinancialAnalyzer()

    # 1. 단일 기업 심화 분석
    print(&quot;  심화 분석 예시&quot;)
    analyzer.analyze_financials_extended(&quot;005930&quot;)  # 삼성전자

    # 2. 여러 기업 비교 분석
    print(&quot;\n&quot; + &quot;=&quot;*80)
    print(&quot;  기업 비교 분석 예시&quot;)

    tech_companies = [&quot;005930&quot;, &quot;000660&quot;, &quot;035420&quot;]  # 삼성전자, SK하이닉스, 네이버
    comparison_result = analyzer.compare_companies(tech_companies)

    if not comparison_result.empty:
        # 특정 지표 기준 랭킹
        try:
            print(f&quot;\n  ROE 기준 순위:&quot;)
            roe_ranking = comparison_result[comparison_result['ROE(자기자본수익률)'] != 'N/A'].copy()
            if not roe_ranking.empty:
                roe_ranking['ROE_숫자'] = roe_ranking['ROE(자기자본수익률)'].str.replace('%', '').astype(float)
                roe_ranking = roe_ranking.sort_values('ROE_숫자', ascending=False)
                for idx, row in roe_ranking.iterrows():
                    print(f&quot;  {idx+1}위: {row['종목코드']} - {row['ROE(자기자본수익률)']}&quot;)
        except Exception as e:
            print(f&quot;랭킹 계산 오류: {e}&quot;)


if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1101</guid>
      <comments>https://ljj777.tistory.com/1101#entry1101comment</comments>
      <pubDate>Thu, 7 Aug 2025 08:44:49 +0900</pubDate>
    </item>
    <item>
      <title>재무재표심화</title>
      <link>https://ljj777.tistory.com/1099</link>
      <description>&lt;pre id=&quot;code_1754487793867&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd
import requests
from typing import Optional, Dict, Any, Tuple
import warnings
warnings.filterwarnings('ignore')

class EnhancedFinancialAnalyzer:
    &quot;&quot;&quot;네이버 금융 재무제표 분석기 - 확장된 재무비율 포함&quot;&quot;&quot;

    def __init__(self):
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }

    def get_financial_statement(self, ticker: str) -&amp;gt; Optional[pd.DataFrame]:
        &quot;&quot;&quot;네이버 금융에서 손익계산서 데이터를 가져오는 함수&quot;&quot;&quot;
        url = f'https://finance.naver.com/item/main.naver?code={ticker}'

        try:
            response = requests.get(url, headers=self.headers, timeout=10)
            if response.status_code != 200:
                print(f&quot;⚠️ 페이지 접근 실패: HTTP {response.status_code}&quot;)
                return None

            tables = pd.read_html(url, encoding='euc-kr', header=0)

        except requests.exceptions.RequestException as e:
            print(f&quot;⚠️ 네트워크 오류: {e}&quot;)
            return None
        except Exception as e:
            print(f&quot;⚠️ 데이터 파싱 오류: {e}&quot;)
            return None

        # 손익계산서 테이블 찾기
        for i, table in enumerate(tables):
            if table.shape[1] &amp;gt;= 3 and len(table) &amp;gt; 5:
                first_col = table.iloc[:, 0].astype(str).str.strip()
                if any('매출' in cell for cell in first_col):
                    print(f&quot;✅ 재무제표 발견 (테이블 #{i+1})&quot;)
                    return table

        print(&quot;⚠️ 손익계산서 테이블을 찾을 수 없습니다&quot;)
        return None

    def get_balance_sheet(self, ticker: str) -&amp;gt; Optional[pd.DataFrame]:
        &quot;&quot;&quot;네이버 금융에서 재무상태표 데이터를 가져오는 함수&quot;&quot;&quot;
        url = f'https://finance.naver.com/item/main.naver?code={ticker}'

        try:
            response = requests.get(url, headers=self.headers, timeout=10)
            if response.status_code != 200:
                return None

            tables = pd.read_html(url, encoding='euc-kr', header=0)

            # 재무상태표 테이블 찾기 (자산, 부채 등이 포함된 테이블)
            for i, table in enumerate(tables):
                if table.shape[1] &amp;gt;= 3 and len(table) &amp;gt; 5:
                    first_col = table.iloc[:, 0].astype(str).str.strip()
                    if any(keyword in cell for keyword in ['자산', '부채', '자본'] for cell in first_col):
                        print(f&quot;✅ 재무상태표 발견 (테이블 #{i+1})&quot;)
                        return table

        except Exception as e:
            print(f&quot;⚠️ 재무상태표 데이터 오류: {e}&quot;)

        return None

    def get_company_info(self, ticker: str) -&amp;gt; Dict[str, Any]:
        &quot;&quot;&quot;기업 기본정보 및 주가 정보 수집&quot;&quot;&quot;
        try:
            # 주식 정보 페이지에서 시가총액, 주가 등 정보 수집
            url = f'https://finance.naver.com/item/main.naver?code={ticker}'
            response = requests.get(url, headers=self.headers, timeout=10)
            
            if response.status_code != 200:
                return {}

            tables = pd.read_html(url, encoding='euc-kr')
            
            # 시가총액, 주가 등 정보가 있는 테이블 찾기
            company_info = {}
            
            for table in tables:
                if len(table.columns) &amp;gt;= 2:
                    # 테이블을 문자열로 변환하여 검색
                    table_str = table.astype(str)
                    if table_str.apply(lambda x: x.str.contains('시가총액|주가|거래량', na=False)).any().any():
                        # 시가총액 정보 추출 시도
                        try:
                            for idx, row in table.iterrows():
                                if '시가총액' in str(row.iloc[0]):
                                    company_info['시가총액'] = str(row.iloc[1])
                                elif '현재가' in str(row.iloc[0]) or '주가' in str(row.iloc[0]):
                                    company_info['현재가'] = str(row.iloc[1])
                        except:
                            continue
                            
            return company_info
            
        except Exception as e:
            print(f&quot;⚠️ 기업정보 수집 오류: {e}&quot;)
            return {}

    def clean_financial_df(self, df: pd.DataFrame) -&amp;gt; pd.DataFrame:
        &quot;&quot;&quot;데이터프레임 전처리&quot;&quot;&quot;
        df_copy = df.copy()
        df_copy.set_index(df_copy.columns[0], inplace=True)
        df_copy = df_copy.replace(['-', '/', 'N/A', '', ' '], '0')

        def convert_to_number(x):
            if pd.isna(x) or x == '':
                return 0
            try:
                if isinstance(x, str):
                    cleaned = x.replace(',', '').replace('(', '-').replace(')', '').strip()
                    return float(cleaned) if cleaned else 0
                return float(x)
            except (ValueError, TypeError):
                return 0

        for col in df_copy.columns:
            df_copy[col] = df_copy[col].apply(convert_to_number)

        return df_copy

    def find_row_name(self, df: pd.DataFrame, candidates: list) -&amp;gt; str:
        &quot;&quot;&quot;유연한 행 이름 매칭&quot;&quot;&quot;
        index_str = df.index.astype(str).str.strip()

        for candidate in candidates:
            if candidate in index_str.values:
                return candidate

            matches = index_str[index_str.str.contains(candidate, na=False)]
            if len(matches) &amp;gt; 0:
                return matches.iloc[0]

        raise KeyError(f&quot;다음 항목을 찾을 수 없습니다: {candidates}&quot;)

    def calculate_growth_rate(self, current: float, previous: float) -&amp;gt; Tuple[str, float]:
        &quot;&quot;&quot;성장률 계산 (문자열과 숫자값 모두 반환)&quot;&quot;&quot;
        try:
            if previous == 0:
                return &quot;N/A (이전값 0)&quot;, 0

            growth_rate = ((current - previous) / abs(previous)) * 100
            return f&quot;{growth_rate:.2f}%&quot;, growth_rate

        except (ZeroDivisionError, TypeError):
            return &quot;N/A&quot;, 0

    def calculate_extended_ratios(self, income_df: pd.DataFrame, balance_df: Optional[pd.DataFrame] = None) -&amp;gt; Dict[str, Any]:
        &quot;&quot;&quot;확장된 재무비율 계산&quot;&quot;&quot;
        ratios = {}
        
        try:
            income_cleaned = self.clean_financial_df(income_df)
            
            # 최신 두 기간 데이터
            if len(income_cleaned.columns) &amp;lt; 2:
                return {'오류': '비교할 데이터가 충분하지 않습니다'}
                
            latest = income_cleaned.columns[-1]
            prev = income_cleaned.columns[-2]

            # 손익계산서 주요 항목
            revenue_current = income_cleaned.loc[self.find_row_name(income_cleaned, ['매출액', '수익(매출액)', '총매출액']), latest]
            revenue_previous = income_cleaned.loc[self.find_row_name(income_cleaned, ['매출액', '수익(매출액)', '총매출액']), prev]
            
            operating_current = income_cleaned.loc[self.find_row_name(income_cleaned, ['영업이익', '영업이익(손실)', '영업손익']), latest]
            operating_previous = income_cleaned.loc[self.find_row_name(income_cleaned, ['영업이익', '영업이익(손실)', '영업손익']), prev]
            
            net_current = income_cleaned.loc[self.find_row_name(income_cleaned, ['당기순이익', '당기순이익(손실)', '순이익', '당기순손익']), latest]
            net_previous = income_cleaned.loc[self.find_row_name(income_cleaned, ['당기순이익', '당기순이익(손실)', '순이익', '당기순손익']), prev]

            # 1. 수익성 비율 (Profitability Ratios)
            if revenue_current != 0:
                ratios['매출총이익률'] = f&quot;{((revenue_current - 0) / revenue_current * 100):.2f}%&quot;  # 매출원가 데이터 필요시 수정
                ratios['영업이익률'] = f&quot;{(operating_current / revenue_current * 100):.2f}%&quot;
                ratios['순이익률'] = f&quot;{(net_current / revenue_current * 100):.2f}%&quot;
                
                # EBITDA 추정 (감가상각비 데이터가 있다면 더 정확)
                try:
                    # 감가상각비 찾기 시도
                    depreciation = 0
                    try:
                        depreciation_row = self.find_row_name(income_cleaned, ['감가상각비', '상각비'])
                        depreciation = income_cleaned.loc[depreciation_row, latest]
                    except KeyError:
                        # 감가상각비를 찾을 수 없으면 영업이익의 10%로 추정
                        depreciation = operating_current * 0.1
                    
                    ebitda = operating_current + depreciation
                    ratios['EBITDA'] = f&quot;{ebitda:,.0f}백만원&quot;
                    ratios['EBITDA마진'] = f&quot;{(ebitda / revenue_current * 100):.2f}%&quot; if revenue_current != 0 else &quot;N/A&quot;
                except:
                    ratios['EBITDA'] = &quot;계산불가&quot;
                    ratios['EBITDA마진'] = &quot;계산불가&quot;

            # 2. 성장성 비율 (Growth Ratios)
            revenue_growth_str, revenue_growth_val = self.calculate_growth_rate(revenue_current, revenue_previous)
            operating_growth_str, operating_growth_val = self.calculate_growth_rate(operating_current, operating_previous)
            net_growth_str, net_growth_val = self.calculate_growth_rate(net_current, net_previous)
            
            ratios['매출액증가율'] = revenue_growth_str
            ratios['영업이익증가율'] = operating_growth_str
            ratios['순이익증가율'] = net_growth_str

            # 재무상태표 기반 비율 (데이터가 있는 경우)
            if balance_df is not None:
                try:
                    balance_cleaned = self.clean_financial_df(balance_df)
                    
                    # 자산 관련
                    total_assets = balance_cleaned.loc[self.find_row_name(balance_cleaned, ['자산총계', '총자산', '자산합계']), latest]
                    
                    # 부채 관련
                    total_liabilities = balance_cleaned.loc[self.find_row_name(balance_cleaned, ['부채총계', '총부채', '부채합계']), latest]
                    
                    # 자본 관련
                    total_equity = balance_cleaned.loc[self.find_row_name(balance_cleaned, ['자본총계', '총자본', '자본합계', '자기자본']), latest]
                    
                    # 3. 안전성 비율 (Stability Ratios)
                    if total_assets != 0:
                        ratios['부채비율'] = f&quot;{(total_liabilities / total_equity * 100):.2f}%&quot; if total_equity != 0 else &quot;N/A&quot;
                        ratios['자기자본비율'] = f&quot;{(total_equity / total_assets * 100):.2f}%&quot;
                        ratios['부채자산비율'] = f&quot;{(total_liabilities / total_assets * 100):.2f}%&quot;
                    
                    # 4. 활동성 비율 (Activity Ratios)
                    if total_assets != 0:
                        ratios['총자산회전율'] = f&quot;{(revenue_current / total_assets):.2f}회&quot;
                    
                    # 5. 수익성 심화 분석
                    if total_assets != 0:
                        ratios['ROA(총자산수익률)'] = f&quot;{(net_current / total_assets * 100):.2f}%&quot;
                    if total_equity != 0:
                        ratios['ROE(자기자본수익률)'] = f&quot;{(net_current / total_equity * 100):.2f}%&quot;
                        
                except KeyError as e:
                    ratios['재무상태표_오류'] = f&quot;재무상태표 항목 부족: {str(e)}&quot;
                except Exception as e:
                    ratios['재무상태표_계산오류'] = str(e)

            # 6. 종합 평가 점수 시스템
            score = 0
            max_score = 0
            
            # 수익성 점수 (40점)
            max_score += 40
            if revenue_current &amp;gt; 0:
                operating_margin = operating_current / revenue_current * 100
                if operating_margin &amp;gt;= 20: score += 15
                elif operating_margin &amp;gt;= 15: score += 12
                elif operating_margin &amp;gt;= 10: score += 8
                elif operating_margin &amp;gt;= 5: score += 4
                elif operating_margin &amp;gt;= 0: score += 1
                
                net_margin = net_current / revenue_current * 100
                if net_margin &amp;gt;= 15: score += 15
                elif net_margin &amp;gt;= 10: score += 12
                elif net_margin &amp;gt;= 5: score += 8
                elif net_margin &amp;gt;= 2: score += 4
                elif net_margin &amp;gt;= 0: score += 1
                
                # EBITDA 마진 평가
                try:
                    ebitda_margin = float(ratios.get('EBITDA마진', '0%').replace('%', ''))
                    if ebitda_margin &amp;gt;= 25: score += 10
                    elif ebitda_margin &amp;gt;= 20: score += 8
                    elif ebitda_margin &amp;gt;= 15: score += 6
                    elif ebitda_margin &amp;gt;= 10: score += 3
                    elif ebitda_margin &amp;gt;= 5: score += 1
                except:
                    pass
            
            # 성장성 점수 (30점)
            max_score += 30
            if revenue_growth_val &amp;gt;= 20: score += 10
            elif revenue_growth_val &amp;gt;= 10: score += 8
            elif revenue_growth_val &amp;gt;= 5: score += 6
            elif revenue_growth_val &amp;gt;= 0: score += 3
            
            if operating_growth_val &amp;gt;= 30: score += 10
            elif operating_growth_val &amp;gt;= 15: score += 8
            elif operating_growth_val &amp;gt;= 5: score += 6
            elif operating_growth_val &amp;gt;= 0: score += 3
            
            if net_growth_val &amp;gt;= 30: score += 10
            elif net_growth_val &amp;gt;= 15: score += 8
            elif net_growth_val &amp;gt;= 5: score += 6
            elif net_growth_val &amp;gt;= 0: score += 3
            
            # 안정성 점수 (30점) - 재무상태표 데이터가 있는 경우만
            if balance_df is not None and '부채비율' in ratios:
                max_score += 30
                try:
                    debt_ratio = float(ratios['부채비율'].replace('%', ''))
                    if debt_ratio &amp;lt;= 30: score += 15
                    elif debt_ratio &amp;lt;= 50: score += 12
                    elif debt_ratio &amp;lt;= 100: score += 8
                    elif debt_ratio &amp;lt;= 200: score += 4
                    elif debt_ratio &amp;lt;= 300: score += 1
                    
                    equity_ratio = float(ratios['자기자본비율'].replace('%', ''))
                    if equity_ratio &amp;gt;= 70: score += 15
                    elif equity_ratio &amp;gt;= 50: score += 12
                    elif equity_ratio &amp;gt;= 30: score += 8
                    elif equity_ratio &amp;gt;= 20: score += 4
                    elif equity_ratio &amp;gt;= 10: score += 1
                except:
                    max_score -= 30

            # 최종 점수 계산
            if max_score &amp;gt; 0:
                final_score = (score / max_score) * 100
                ratios['종합점수'] = f&quot;{final_score:.1f}점 ({score}/{max_score})&quot;
                
                if final_score &amp;gt;= 80:
                    ratios['투자등급'] = &quot;  우수 (A급)&quot;
                elif final_score &amp;gt;= 65:
                    ratios['투자등급'] = &quot;  양호 (B급)&quot;
                elif final_score &amp;gt;= 50:
                    ratios['투자등급'] = &quot;  보통 (C급)&quot;
                elif final_score &amp;gt;= 35:
                    ratios['투자등급'] = &quot;  주의 (D급)&quot;
                else:
                    ratios['투자등급'] = &quot;  위험 (E급)&quot;
            else:
                ratios['종합점수'] = &quot;계산불가&quot;
                ratios['투자등급'] = &quot;평가불가&quot;

        except Exception as e:
            ratios['계산오류'] = str(e)

        return ratios

    def analyze_financials_extended(self, ticker: str) -&amp;gt; Dict[str, Any]:
        &quot;&quot;&quot;확장된 재무 분석&quot;&quot;&quot;
        print(f&quot;\n{'='*60}&quot;)
        print(f&quot;  [{ticker}] 심화 재무제표 분석 보고서&quot;)
        print(f&quot;{'='*60}&quot;)

        # 손익계산서 데이터 수집
        income_df = self.get_financial_statement(ticker)
        if income_df is None:
            return {'오류': '손익계산서 데이터를 가져올 수 없습니다'}

        # 재무상태표 데이터 수집 (선택적)
        balance_df = self.get_balance_sheet(ticker)
        if balance_df is not None:
            print(&quot;✅ 재무상태표 데이터도 확보됨 - 더 정확한 분석 가능&quot;)
        else:
            print(&quot;⚠️ 재무상태표 데이터 없음 - 손익계산서 중심 분석&quot;)

        # 기업 정보 수집
        company_info = self.get_company_info(ticker)

        # 확장된 재무비율 계산
        ratios = self.calculate_extended_ratios(income_df, balance_df)

        # 기본 정보 출력
        print(f&quot;\n  기업 기본정보:&quot;)
        print(&quot;-&quot; * 30)
        for key, value in company_info.items():
            print(f&quot;{key}: {value}&quot;)

        # 재무비율 출력
        print(f&quot;\n  재무비율 분석 결과:&quot;)
        print(&quot;-&quot; * 30)

        # 카테고리별로 구분하여 출력
        categories = {
            '  수익성 지표': ['매출총이익률', '영업이익률', '순이익률', 'EBITDA', 'EBITDA마진', 'ROA(총자산수익률)', 'ROE(자기자본수익률)'],
            '  성장성 지표': ['매출액증가율', '영업이익증가율', '순이익증가율'],
            ' ️ 안정성 지표': ['부채비율', '자기자본비율', '부채자산비율'],
            '  활동성 지표': ['총자산회전율'],
            '⭐ 종합평가': ['종합점수', '투자등급']
        }

        for category, metrics in categories.items():
            category_ratios = {k: v for k, v in ratios.items() if k in metrics}
            if category_ratios:
                print(f&quot;\n{category}:&quot;)
                for metric, value in category_ratios.items():
                    print(f&quot;  &amp;bull; {metric}: {value}&quot;)

        # 기타 정보 출력
        other_ratios = {k: v for k, v in ratios.items() 
                       if k not in sum(categories.values(), []) 
                       and not k.endswith('오류')}
        
        if other_ratios:
            print(f&quot;\n  추가 정보:&quot;)
            for key, value in other_ratios.items():
                print(f&quot;  &amp;bull; {key}: {value}&quot;)

        # 오류 정보 출력
        error_ratios = {k: v for k, v in ratios.items() if k.endswith('오류')}
        if error_ratios:
            print(f&quot;\n⚠️ 분석 제한사항:&quot;)
            for key, value in error_ratios.items():
                print(f&quot;  &amp;bull; {key}: {value}&quot;)

        print(f&quot;\n{'='*60}&quot;)
        return ratios

    def compare_companies(self, tickers: list) -&amp;gt; pd.DataFrame:
        &quot;&quot;&quot;여러 기업 재무비율 비교&quot;&quot;&quot;
        print(f&quot;\n{'='*70}&quot;)
        print(f&quot;  기업 비교 분석 ({len(tickers)}개 기업)&quot;)
        print(f&quot;{'='*70}&quot;)
        
        comparison_data = []
        
        for ticker in tickers:
            print(f&quot;\n  {ticker} 분석 중...&quot;)
            
            income_df = self.get_financial_statement(ticker)
            if income_df is None:
                continue
                
            balance_df = self.get_balance_sheet(ticker)
            ratios = self.calculate_extended_ratios(income_df, balance_df)
            
            # 비교용 데이터 추출
            company_data = {'종목코드': ticker}
            
            # 주요 지표만 선택
            key_metrics = ['영업이익률', '순이익률', 'ROE(자기자본수익률)', 'ROA(총자산수익률)', 
                          '매출액증가율', '영업이익증가율', '부채비율', '자기자본비율', '종합점수', '투자등급']
            
            for metric in key_metrics:
                company_data[metric] = ratios.get(metric, 'N/A')
            
            comparison_data.append(company_data)
        
        # 데이터프레임 생성
        if comparison_data:
            comparison_df = pd.DataFrame(comparison_data)
            print(f&quot;\n  비교 결과:&quot;)
            print(&quot;-&quot; * 70)
            print(comparison_df.to_string(index=False))
            return comparison_df
        else:
            print(&quot;❌ 비교할 데이터가 없습니다&quot;)
            return pd.DataFrame()


def main():
    analyzer = EnhancedFinancialAnalyzer()

    # 1. 단일 기업 심화 분석
    print(&quot;  심화 분석 예시&quot;)
    analyzer.analyze_financials_extended(&quot;005930&quot;)  # 삼성전자

    # 2. 여러 기업 비교 분석
    print(&quot;\n&quot; + &quot;=&quot;*80)
    print(&quot;  기업 비교 분석 예시&quot;)
    
    tech_companies = [&quot;005930&quot;, &quot;000660&quot;, &quot;035420&quot;]  # 삼성전자, SK하이닉스, 네이버
    comparison_result = analyzer.compare_companies(tech_companies)
    
    if not comparison_result.empty:
        # 특정 지표 기준 랭킹
        try:
            print(f&quot;\n  ROE 기준 순위:&quot;)
            roe_ranking = comparison_result[comparison_result['ROE(자기자본수익률)'] != 'N/A'].copy()
            if not roe_ranking.empty:
                roe_ranking['ROE_숫자'] = roe_ranking['ROE(자기자본수익률)'].str.replace('%', '').astype(float)
                roe_ranking = roe_ranking.sort_values('ROE_숫자', ascending=False)
                for idx, row in roe_ranking.iterrows():
                    print(f&quot;  {idx+1}위: {row['종목코드']} - {row['ROE(자기자본수익률)']}&quot;)
        except Exception as e:
            print(f&quot;랭킹 계산 오류: {e}&quot;)


if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/python</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1099</guid>
      <comments>https://ljj777.tistory.com/1099#entry1099comment</comments>
      <pubDate>Wed, 6 Aug 2025 22:43:49 +0900</pubDate>
    </item>
    <item>
      <title>재무재표개선</title>
      <link>https://ljj777.tistory.com/1098</link>
      <description>&lt;pre id=&quot;code_1754486088286&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd
import requests
from typing import Optional, Dict, Any
import warnings
warnings.filterwarnings('ignore')

class FinancialAnalyzer:
    &quot;&quot;&quot;네이버 금융 재무제표 분석기&quot;&quot;&quot;
    
    def __init__(self):
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }
    
    def get_financial_statement(self, ticker: str) -&amp;gt; Optional[pd.DataFrame]:
        &quot;&quot;&quot;
        네이버 금융에서 손익계산서 데이터를 가져오는 함수
        
        Args:
            ticker (str): 종목 코드 (예: '005930')
            
        Returns:
            pd.DataFrame or None: 재무제표 데이터
        &quot;&quot;&quot;
        url = f'https://finance.naver.com/item/main.naver?code={ticker}'
        
        try:
            # requests로 먼저 페이지 확인
            response = requests.get(url, headers=self.headers, timeout=10)
            if response.status_code != 200:
                print(f&quot;⚠️ 페이지 접근 실패: HTTP {response.status_code}&quot;)
                return None
                
            # 한글 인코딩 처리 개선
            tables = pd.read_html(url, encoding='euc-kr', header=0)
            
        except requests.exceptions.RequestException as e:
            print(f&quot;⚠️ 네트워크 오류: {e}&quot;)
            return None
        except Exception as e:
            print(f&quot;⚠️ 데이터 파싱 오류: {e}&quot;)
            return None

        # 손익계산서 테이블 찾기
        for i, table in enumerate(tables):
            if table.shape[1] &amp;gt;= 3 and len(table) &amp;gt; 5:
                # 매출액이 포함된 테이블 찾기
                first_col = table.iloc[:, 0].astype(str).str.strip()
                if any('매출' in cell for cell in first_col):
                    print(f&quot;✅ 재무제표 발견 (테이블 #{i+1})&quot;)
                    return table
                    
        print(&quot;⚠️ 손익계산서 테이블을 찾을 수 없습니다&quot;)
        return None

    def clean_financial_df(self, df: pd.DataFrame) -&amp;gt; pd.DataFrame:
        &quot;&quot;&quot;
        데이터프레임 전처리: 인덱싱, 숫자 변환, 결측치 처리 등
        
        Args:
            df (pd.DataFrame): 원본 재무제표 데이터
            
        Returns:
            pd.DataFrame: 정제된 데이터
        &quot;&quot;&quot;
        df_copy = df.copy()
        
        # 첫 번째 컬럼을 인덱스로 설정
        df_copy.set_index(df_copy.columns[0], inplace=True)
        
        # 결측치 및 특수 문자 처리
        df_copy = df_copy.replace(['-', '/', 'N/A', '', ' '], '0')
        
        # 숫자 변환 함수
        def convert_to_number(x):
            if pd.isna(x) or x == '':
                return 0
            try:
                # 문자열인 경우 콤마 제거 후 숫자 변환
                if isinstance(x, str):
                    cleaned = x.replace(',', '').replace('(', '-').replace(')', '').strip()
                    return float(cleaned) if cleaned else 0
                return float(x)
            except (ValueError, TypeError):
                return 0
        
        # 모든 컬럼에 숫자 변환 적용
        for col in df_copy.columns:
            df_copy[col] = df_copy[col].apply(convert_to_number)
            
        return df_copy

    def find_row_name(self, df: pd.DataFrame, candidates: list) -&amp;gt; str:
        &quot;&quot;&quot;
        유연한 행 이름 매칭
        
        Args:
            df (pd.DataFrame): 데이터프레임
            candidates (list): 후보 행 이름들
            
        Returns:
            str: 매칭된 행 이름
        &quot;&quot;&quot;
        index_str = df.index.astype(str).str.strip()
        
        for candidate in candidates:
            # 정확히 일치하는 경우
            if candidate in index_str.values:
                return candidate
            
            # 부분 매칭 (포함 관계)
            matches = index_str[index_str.str.contains(candidate, na=False)]
            if len(matches) &amp;gt; 0:
                return matches.iloc[0]
        
        raise KeyError(f&quot;다음 항목을 찾을 수 없습니다: {candidates}&quot;)

    def calculate_growth_rate(self, current: float, previous: float) -&amp;gt; str:
        &quot;&quot;&quot;
        성장률 계산
        
        Args:
            current (float): 현재 값
            previous (float): 이전 값
            
        Returns:
            str: 성장률 (%)
        &quot;&quot;&quot;
        try:
            if previous == 0:
                return &quot;N/A (이전값 0)&quot;
            
            growth_rate = ((current - previous) / abs(previous)) * 100
            return f&quot;{growth_rate:.2f}%&quot;
            
        except (ZeroDivisionError, TypeError):
            return &quot;N/A&quot;

    def analyze_financials(self, df: pd.DataFrame) -&amp;gt; Dict[str, Any]:
        &quot;&quot;&quot;
        주요 지표 분석 및 해석 제공
        
        Args:
            df (pd.DataFrame): 재무제표 데이터
            
        Returns:
            dict: 분석 결과
        &quot;&quot;&quot;
        analysis = {}
        
        try:
            df_cleaned = self.clean_financial_df(df)
            
            # 최신 두 기간 선택
            if len(df_cleaned.columns) &amp;lt; 2:
                raise ValueError(&quot;비교할 데이터가 충분하지 않습니다 (최소 2개 기간 필요)&quot;)
            
            latest = df_cleaned.columns[-1]
            prev = df_cleaned.columns[-2]
            
            print(f&quot;  비교 기간: {prev} vs {latest}&quot;)
            
            # 주요 항목 찾기
            try:
                row_revenue = self.find_row_name(df_cleaned, ['매출액', '수익(매출액)', '총매출액'])
                row_operating = self.find_row_name(df_cleaned, ['영업이익', '영업이익(손실)', '영업손익'])
                row_net = self.find_row_name(df_cleaned, ['당기순이익', '당기순이익(손실)', '순이익', '당기순손익'])
            except KeyError as e:
                analysis['오류'] = str(e)
                return analysis
            
            # 데이터 추출 (백만원 단위)
            revenue_current = df_cleaned.loc[row_revenue, latest]
            revenue_previous = df_cleaned.loc[row_revenue, prev]
            
            operating_current = df_cleaned.loc[row_operating, latest]
            operating_previous = df_cleaned.loc[row_operating, prev]
            
            net_current = df_cleaned.loc[row_net, latest]
            net_previous = df_cleaned.loc[row_net, prev]
            
            # 기본 정보
            analysis['  매출액 현재'] = f&quot;{revenue_current:,.0f}백만원&quot;
            analysis['  매출액 이전'] = f&quot;{revenue_previous:,.0f}백만원&quot;
            analysis['  매출액 변화'] = '증가' if revenue_current &amp;gt; revenue_previous else '감소'
            analysis['  매출액 증감률'] = self.calculate_growth_rate(revenue_current, revenue_previous)
            
            analysis['  영업이익 현재'] = f&quot;{operating_current:,.0f}백만원&quot;
            analysis['  영업이익 이전'] = f&quot;{operating_previous:,.0f}백만원&quot;
            analysis['  영업이익 변화'] = '증가' if operating_current &amp;gt; operating_previous else '감소'
            analysis['  영업이익 증감률'] = self.calculate_growth_rate(operating_current, operating_previous)
            
            analysis['  순이익 현재'] = f&quot;{net_current:,.0f}백만원&quot;
            analysis['  순이익 이전'] = f&quot;{net_previous:,.0f}백만원&quot;
            analysis['  순이익 변화'] = '증가' if net_current &amp;gt; net_previous else '감소'
            analysis['  순이익 증감률'] = self.calculate_growth_rate(net_current, net_previous)
            
            # 수익성 지표 계산
            if revenue_current != 0:
                operating_margin = (operating_current / revenue_current) * 100
                net_margin = (net_current / revenue_current) * 100
                
                analysis['  영업이익률'] = f&quot;{operating_margin:.2f}%&quot;
                analysis['  순이익률'] = f&quot;{net_margin:.2f}%&quot;
                
                # 영업이익률 평가
                if operating_margin &amp;gt;= 15:
                    analysis['⭐ 영업이익률 평가'] = '매우 우수'
                elif operating_margin &amp;gt;= 10:
                    analysis['⭐ 영업이익률 평가'] = '우수'
                elif operating_margin &amp;gt;= 5:
                    analysis['⭐ 영업이익률 평가'] = '보통'
                elif operating_margin &amp;gt;= 0:
                    analysis['⭐ 영업이익률 평가'] = '낮음'
                else:
                    analysis['⭐ 영업이익률 평가'] = '적자'
            
            # 종합 평가
            positive_signals = 0
            if revenue_current &amp;gt; revenue_previous:
                positive_signals += 1
            if operating_current &amp;gt; operating_previous:
                positive_signals += 1
            if net_current &amp;gt; net_previous:
                positive_signals += 1
                
            if positive_signals == 3:
                analysis['  종합평가'] = '매우 긍정적 (3/3 지표 개선)'
            elif positive_signals == 2:
                analysis['  종합평가'] = '긍정적 (2/3 지표 개선)'
            elif positive_signals == 1:
                analysis['  종합평가'] = '혼조 (1/3 지표 개선)'
            else:
                analysis['  종합평가'] = '부정적 (모든 지표 악화)'
            
            # 데이터 유형 추정
            column_names = ' '.join(df_cleaned.columns.astype(str))
            if any(keyword in column_names for keyword in ['년', 'Year', '연간']):
                analysis['  데이터 유형'] = '연간 실적'
            else:
                analysis['  데이터 유형'] = '분기별 실적'
                
        except Exception as e:
            analysis['❌ 분석 오류'] = str(e)
            
        return analysis

    def print_analysis(self, ticker: str):
        &quot;&quot;&quot;
        재무 분석 결과 출력
        
        Args:
            ticker (str): 종목 코드
        &quot;&quot;&quot;
        print(f&quot;\n{'='*50}&quot;)
        print(f&quot;  [{ticker}] 재무제표 분석 보고서&quot;)
        print(f&quot;{'='*50}&quot;)
        
        df = self.get_financial_statement(ticker)
        
        if df is None:
            print(&quot;❌ 재무제표 데이터를 가져올 수 없습니다&quot;)
            print(&quot;   - 종목 코드가 올바른지 확인해주세요&quot;)
            print(&quot;   - 네트워크 연결 상태를 확인해주세요&quot;)
            return
        
        print(f&quot;✅ 데이터 로드 완료 (행: {len(df)}, 열: {len(df.columns)})&quot;)
        
        analysis_result = self.analyze_financials(df)
        
        print(f&quot;\n  분석 결과:&quot;)
        print(&quot;-&quot; * 40)
        
        for key, value in analysis_result.items():
            print(f&quot;{key}: {value}&quot;)
        
        print(f&quot;\n{'='*50}&quot;)


# 사용 예시
def main():
    analyzer = FinancialAnalyzer()
    
    # 여러 종목 분석 예시
    tickers = [
        &quot;005930&quot;,  # 삼성전자
        &quot;000660&quot;,  # SK하이닉스  
        &quot;035420&quot;,  # NAVER
        &quot;005380&quot;,  # 현대차
    ]
    
    for ticker in tickers:
        try:
            analyzer.print_analysis(ticker)
            print(&quot;\n&quot; + &quot;=&quot;*60 + &quot;\n&quot;)
        except KeyboardInterrupt:
            print(&quot;\n사용자에 의해 중단되었습니다.&quot;)
            break
        except Exception as e:
            print(f&quot;❌ {ticker} 분석 중 오류: {e}&quot;)
            continue


if __name__ == &quot;__main__&quot;:
    # 단일 종목 분석
    analyzer = FinancialAnalyzer()
    analyzer.print_analysis(&quot;005930&quot;)  # 삼성전자
    
    # 또는 여러 종목 분석
    # main()&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/python</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1098</guid>
      <comments>https://ljj777.tistory.com/1098#entry1098comment</comments>
      <pubDate>Wed, 6 Aug 2025 22:15:08 +0900</pubDate>
    </item>
    <item>
      <title>재무제표 분석</title>
      <link>https://ljj777.tistory.com/1097</link>
      <description>&lt;pre id=&quot;code_1754483456571&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd
import requests

def get_financial_statement(ticker):
    &quot;&quot;&quot;
    네이버 금융에서 손익계산서 데이터를 가져오는 함수
    &quot;&quot;&quot;
    url = f'https://finance.naver.com/item/main.naver?code={ticker}'
    try:
        tables = pd.read_html(url, encoding='euc-kr')
    except:
        return None

    for table in tables:
        if table.shape[1] &amp;gt;= 3 and '매출액' in table.iloc[:, 0].values:
            return table
    return None

def clean_financial_df(df):
    &quot;&quot;&quot;
    데이터프레임 전처리: 인덱싱, 숫자 변환, 결측치 처리 등
    &quot;&quot;&quot;
    df.set_index(df.columns[0], inplace=True)
    df = df.replace('-', '0')  # 결측치는 0으로
    df = df.applymap(lambda x: int(str(x).replace(',', '')) if isinstance(x, str) else x)
    return df

def analyze_financials(df):
    &quot;&quot;&quot;
    주요 지표 분석 및 해석 제공
    &quot;&quot;&quot;
    analysis = {}

    try:
        df_cleaned = clean_financial_df(df)

        # 컬럼(연도 또는 분기) 자동 탐지
        latest = df_cleaned.columns[-1]
        prev = df_cleaned.columns[-2]

        # 유연한 행 이름 처리 (예: '영업이익(손실)', '당기순이익(손실)')
        def get_row_name(df, candidates):
            for candidate in candidates:
                if candidate in df.index:
                    return candidate
            raise ValueError(f&quot;다음 항목을 찾을 수 없습니다: {candidates}&quot;)

        row_매출 = get_row_name(df_cleaned, ['매출액'])
        row_영업이익 = get_row_name(df_cleaned, ['영업이익', '영업이익(손실)'])
        row_순이익 = get_row_name(df_cleaned, ['당기순이익', '당기순이익(손실)'])

        # 주요 지표 추출
        revenue_now = df_cleaned.loc[row_매출, latest]
        revenue_prev = df_cleaned.loc[row_매출, prev]

        op_now = df_cleaned.loc[row_영업이익, latest]
        op_prev = df_cleaned.loc[row_영업이익, prev]

        net_now = df_cleaned.loc[row_순이익, latest]
        net_prev = df_cleaned.loc[row_순이익, prev]

        # 증가 여부 판단
        analysis['매출액 증가 여부'] = '  증가' if revenue_now &amp;gt; revenue_prev else '  감소'
        analysis['영업이익 증가 여부'] = '  증가' if op_now &amp;gt; op_prev else '  감소'
        analysis['당기순이익 증가 여부'] = '  증가' if net_now &amp;gt; net_prev else '  감소'

        # 증가율(%) 계산
        def calc_rate(now, prev):
            try:
                if prev == 0:
                    return &quot;N/A&quot;
                return f&quot;{((now - prev) / abs(prev)) * 100:.2f}%&quot;
            except:
                return &quot;N/A&quot;

        analysis['매출액 증가율'] = calc_rate(revenue_now, revenue_prev)
        analysis['영업이익 증가율'] = calc_rate(op_now, op_prev)
        analysis['당기순이익 증가율'] = calc_rate(net_now, net_prev)

        # 영업이익률
        if revenue_now != 0:
            op_margin = op_now / revenue_now * 100
            analysis['영업이익률'] = f&quot;{op_margin:.2f}%&quot;

            if op_margin &amp;gt;= 10:
                analysis['영업이익률 평가'] = '✅ 우수'
            elif op_margin &amp;gt;= 5:
                analysis['영업이익률 평가'] = '⚠️ 보통'
            else:
                analysis['영업이익률 평가'] = '❌ 낮음'
        else:
            analysis['영업이익률'] = &quot;N/A&quot;

        # 연간/분기 추정 힌트
        if any(&quot;년&quot; in str(col) for col in df.columns):
            analysis['데이터 유형'] = '  연간 실적'
        else:
            analysis['데이터 유형'] = '  분기 실적 (추정)'

    except Exception as e:
        analysis['에러'] = f&quot;⚠️ 분석 실패: {str(e)}&quot;

    return analysis

def print_analysis(ticker):
    print(f&quot;[{ticker}] 재무 분석 요약&quot;)
    df = get_financial_statement(ticker)

    if df is None:
        print(&quot;❌ 재무제표 데이터 로딩 실패 또는 해당 종목 없음&quot;)
        return
    
    result = analyze_financials(df)
    for k, v in result.items():
        print(f&quot;{k}: {v}&quot;)

# 예시 실행: 삼성전자(005930)
if __name__ == &quot;__main__&quot;:
    print_analysis(&quot;005930&quot;)  # 원하는 종목 코드 입력&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/python</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1097</guid>
      <comments>https://ljj777.tistory.com/1097#entry1097comment</comments>
      <pubDate>Wed, 6 Aug 2025 21:17:28 +0900</pubDate>
    </item>
    <item>
      <title>df a,b,c컬럼을 groupby 하고 df의 d컬럼의 최빈값을 취하고 동률일경우 min값을 가져옴</title>
      <link>https://ljj777.tistory.com/1096</link>
      <description>&lt;pre id=&quot;code_1754025231804&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd

# 예시 데이터프레임 생성 (실제 사용시에는 주석 처리)
data = {
    'a': [1, 1, 1, 2, 2, 2],
    'b': ['x', 'x', 'y', 'y', 'y', 'y'],
    'c': [10, 10, 20, 20, 20, 30],
    'd': [100, 100, 200, 300, 300, 400]
}
df = pd.DataFrame(data)

# 그룹별로 d 컬럼의 최빈값 계산 (동률일 경우 최소값 선택)
result = df.groupby(['a', 'b', 'c'])['d'].agg(
    lambda x: x.mode().min() if not x.mode().empty else None
).reset_index()

print(result)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1096</guid>
      <comments>https://ljj777.tistory.com/1096#entry1096comment</comments>
      <pubDate>Fri, 1 Aug 2025 14:13:54 +0900</pubDate>
    </item>
    <item>
      <title>pandas cross join</title>
      <link>https://ljj777.tistory.com/1095</link>
      <description>&lt;pre id=&quot;code_1754024752410&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd

# 예제 데이터 생성
df1 = pd.DataFrame({
    'a': [1, 2, 3],
    'b': ['x', 'y', 'z'],
    'c': [0.1, 0.2, 0.3]
})

df2 = pd.DataFrame({
    'd': [10, 20]
})

# 크로스 조인 수행 (방법 1)
cross_join = df1.assign(key=1).merge(df2.assign(key=1), on='key').drop('key', axis=1)

# 크로스 조인 수행 (방법 2 - pandas 1.2.0+)
cross_join = df1.merge(df2, how='cross')

print(cross_join)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1095</guid>
      <comments>https://ljj777.tistory.com/1095#entry1095comment</comments>
      <pubDate>Fri, 1 Aug 2025 14:05:55 +0900</pubDate>
    </item>
    <item>
      <title>그룹별 최신 유효값으로 결측값 채우기</title>
      <link>https://ljj777.tistory.com/1094</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;**✅ 기능**: 주어진 데이터프레임에서 지정한 그룹 컬럼 기준으로, **유효한 값이 있는 가장 최신 주차의 데이터**로 결측값을 채웁니다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** ️ 안전성**: 최신 주차 값이 `NaN`이어도 그 다음 최신 유효값을 자동으로 찾아 처리합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1753967540987&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd
import numpy as np

# ------------------------------
# 1. 예제 데이터 생성
# ------------------------------
df = pd.DataFrame({
    'a': ['x', 'x', 'x', 'x', 'y', 'y'],  # 그룹 컬럼 1
    'b': ['u', 'u', 'u', 'u', 'v', 'v'],  # 그룹 컬럼 2
    'c': ['p', 'p', 'p', 'p', 'q', 'q'],  # 그룹 컬럼 3
    '주차': [202410, 202411, 202412, 202413, 202411, 202412],  # 시간 순서 컬럼
    'd': [10, np.nan, 30, np.nan, np.nan, 60]  # 결측값이 있는 타겟 컬럼
})

print(&quot;▶ 원본 데이터:&quot;)
print(df)

# ------------------------------
# 2. 결측값 채우기 함수 정의 (안전한 버전)
# ------------------------------
def fillna_with_latest_valid(df, group_cols, week_col, value_col):
    &quot;&quot;&quot;
      기능: 각 그룹별로 유효한 값이 있는 가장 최신 주차의 데이터로 결측값을 채움
    
    Parameters:
        df (pd.DataFrame): 입력 데이터프레임
        group_cols (list): 그룹화할 컬럼 리스트 (예: ['a','b','c'])
        week_col (str): 시간 순서 컬럼 (예: '주차')
        value_col (str): 결측값을 채울 타겟 컬럼 (예: 'd')
    
    Returns:
        pd.DataFrame: 결측값이 채워진 데이터프레임
    &quot;&quot;&quot;
    # STEP 1. 유효한 값만 필터링 &amp;rarr; 주차 순 정렬 &amp;rarr; 그룹별 최신 값 추출
    latest_values = (
        df.dropna(subset=[value_col])  # 결측값 행 제외
          .sort_values(week_col)       # 주차 오름차순 정렬
          .groupby(group_cols, as_index=False)
          .last()                     # 각 그룹의 마지막 행(최신 주차) 선택
          [group_cols + [value_col]]   # 필요한 컬럼만 추출
          .rename(columns={value_col: 'latest_val'})  # 컬럼명 변경
    )
    
    # STEP 2. 원본 데이터와 병합 후 결측값 채우기
    df_filled = (
        df.merge(latest_values, on=group_cols, how='left')  # 그룹 키로 병합
          .assign(**{value_col: lambda x: x[value_col].fillna(x['latest_val'])})  # 결측값 채우기
          .drop(columns='latest_val')  # 임시 컬럼 제거
    )
    
    return df_filled

# ------------------------------
# 3. 함수 실행 및 결과 비교
# ------------------------------
# ✅ 안전한 버전 실행
df_result_safe = fillna_with_latest_valid(
    df, 
    group_cols=['a', 'b', 'c'], 
    week_col='주차', 
    value_col='d'
)

print(&quot;\n▶ 안전한 버전 적용 결과:&quot;)
print(df_result_safe)

# ------------------------------
# 4. 기존 코드 vs 개선 코드 비교
# ------------------------------
#   주목할 점: (x,u,p) 그룹의 202411주차 결측값 처리 차이
print(&quot;\n  비교 테이블 (기존 코드 vs 개선 코드):&quot;)
comparison = pd.DataFrame({
    '원본_d': df['d'],
    '기존코드결과': [10, np.nan, 30, 30, 60, 60],  # 202411주차 NaN 유지
    '개선코드결과': df_result_safe['d']           # 202411주차 10.0으로 채워짐
}, index=df['주차'])
print(comparison)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1094</guid>
      <comments>https://ljj777.tistory.com/1094#entry1094comment</comments>
      <pubDate>Thu, 31 Jul 2025 22:12:23 +0900</pubDate>
    </item>
    <item>
      <title>max</title>
      <link>https://ljj777.tistory.com/1092</link>
      <description>&lt;pre id=&quot;code_1753926525010&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.max(['d'])

max()는 aggregation 함수인데, 여기에 리스트 ['d']를 넘기면 의미가 없음
&amp;rarr; 이는 Pandas 내부적으로 무시되며, numeric_only=True인 기본 동작으로 numeric 컬럼만 집계합니다.
따라서 d가 숫자형이 아닌 경우나 f만 숫자형이라면 d는 사라집니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1092</guid>
      <comments>https://ljj777.tistory.com/1092#entry1092comment</comments>
      <pubDate>Thu, 31 Jul 2025 10:50:31 +0900</pubDate>
    </item>
    <item>
      <title>max</title>
      <link>https://ljj777.tistory.com/1091</link>
      <description>&lt;pre id=&quot;code_1753926525010&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.max(['d'])

max()는 aggregation 함수인데, 여기에 리스트 ['d']를 넘기면 의미가 없음
&amp;rarr; 이는 Pandas 내부적으로 무시되며, numeric_only=True인 기본 동작으로 numeric 컬럼만 집계합니다.
따라서 d가 숫자형이 아닌 경우나 f만 숫자형이라면 d는 사라집니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1091</guid>
      <comments>https://ljj777.tistory.com/1091#entry1091comment</comments>
      <pubDate>Thu, 31 Jul 2025 10:50:30 +0900</pubDate>
    </item>
    <item>
      <title>dataframe 타입지정</title>
      <link>https://ljj777.tistory.com/1090</link>
      <description>&lt;pre id=&quot;code_1753865426433&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def process_dataframe_optimized(dict_df_types, df):
    type_handlers = {
        'int': lambda s: pd.to_numeric(s, errors='coerce').fillna(0).astype('int32'),
        'float': lambda s: pd.to_numeric(s, errors='coerce').fillna(0).astype('float32'),
        'bool': lambda s: s.astype(str).str.lower().isin(['true', 't', '1']),
        'datetime': lambda s: pd.to_datetime(s, errors='coerce'),
        'string': lambda s: s.astype('string').fillna(''),  # 빈 문자열로 채우기
        'category': lambda s: s.fillna('').astype('category')  # 빈 문자열로 채우기
    }
    
    # 교집합으로 존재하는 컬럼만 선택 (속도 향상)
    valid_cols = set(df.columns) &amp;amp; set(dict_df_types.keys())
    
    # 한 번에 모든 컬럼 처리 (assign 사용)
    return df.assign(**{
        col: type_handlers[dtype](df[col]) 
        for col, dtype in dict_df_types.items() 
        if col in valid_cols and dtype in type_handlers
    })&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1090</guid>
      <comments>https://ljj777.tistory.com/1090#entry1090comment</comments>
      <pubDate>Wed, 30 Jul 2025 17:50:50 +0900</pubDate>
    </item>
    <item>
      <title>flutter clean</title>
      <link>https://ljj777.tistory.com/1089</link>
      <description>&lt;pre id=&quot;code_1753515347433&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd ~/StudioProjects/stock-flutter
flutter clean
flutter pub get
cd android
./gradlew --stop
./gradlew clean
cd ..
flutter run -d emulator-5554&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1089</guid>
      <comments>https://ljj777.tistory.com/1089#entry1089comment</comments>
      <pubDate>Sat, 26 Jul 2025 16:36:16 +0900</pubDate>
    </item>
    <item>
      <title>컬럼 유무 및 NaN 검사를 고려한 안전한 최대값 추출</title>
      <link>https://ljj777.tistory.com/1088</link>
      <description>&lt;pre id=&quot;code_1753658193670&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df1 = df.groupby(by=['a'], as_index=False).max()  # 또는 다른 집계 함수
df['a'] = np.nan if df1['a'].isnull().any() else df1['a'].max()&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1753479363989&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 컬럼 'a'가 있고, NaN이 아닌 값이 하나라도 있으면 최대값, 아니면 np.nan
df['a'] = df1['a'].max() if 'a' in df1.columns and df1['a'].notna().any() else np.nan&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot;&gt;  &lt;/span&gt;EX)&lt;/p&gt;
&lt;pre id=&quot;code_1753498217785&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd
import numpy as np

# 예제 1: 'a' 컬럼이 있고, NaN이 아닌 값이 존재하는 경우
df1 = pd.DataFrame({'a': [3, 7, np.nan]})
df = pd.DataFrame(index=range(3))  # 결과를 저장할 df (빈 3행짜리)

# 적용
df['a'] = df1['a'].max() if 'a' in df1.columns and df1['a'].notna().any() else np.nan
print(&quot;예제 1 결과:\n&quot;, df)

# 예제 2: 'a' 컬럼이 있지만 모든 값이 NaN인 경우
df1 = pd.DataFrame({'a': [np.nan, np.nan]})
df = pd.DataFrame(index=range(3))

df['a'] = df1['a'].max() if 'a' in df1.columns and df1['a'].notna().any() else np.nan
print(&quot;\n예제 2 결과:\n&quot;, df)

# 예제 3: 'a' 컬럼이 존재하지 않는 경우
df1 = pd.DataFrame({'b': [1, 2, 3]})
df = pd.DataFrame(index=range(3))

df['a'] = df1['a'].max() if 'a' in df1.columns and df1['a'].notna().any() else np.nan
print(&quot;\n예제 3 결과:\n&quot;, df)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1088</guid>
      <comments>https://ljj777.tistory.com/1088#entry1088comment</comments>
      <pubDate>Sat, 26 Jul 2025 06:36:15 +0900</pubDate>
    </item>
    <item>
      <title>전체에 null이 하나라도 있으면 np.nan, 아니면 최대값</title>
      <link>https://ljj777.tistory.com/1087</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;시작&lt;/p&gt;
&lt;pre id=&quot;code_1753451622430&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ❌ 잘못된 원래 코드 (에러 발생)
df['a'] = df1['a'].agg(lambda x: np.nan if x.isnull.any() else x.max()).reset_index(drop=True)

# &quot;전체에 null이 하나라도 있으면 np.nan, 아니면 최대값&quot;
# ✅ 올바르고 간결한 코드
df['a'] = np.nan if df1['a'].isnull().any() else df1['a'].max()
# 결론: 맞습니다! 이게 가장 pandas다운 깔끔한 코드입니다.
# 복잡한 agg() 체이닝 대신 조건부 표현식 + 자동 브로드캐스팅을 활용한 완벽한 해결책&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  결론 : 최종선택&lt;/p&gt;
&lt;pre id=&quot;code_1753478522665&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if 'a' in df1.columns:
    # NaN이 아닌 값이 하나라도 있으면 최대값, 아니면 np.nan
    df['a'] = df1['a'].max() if df1['a'].notna().any() else np.nan
else:
    df['a'] = np.nan&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;=========결론 최종선택까지 풀이과정==================================================&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &quot;지금 구조가 실무적으로 합리적입니다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 풀어서 복잡하게 쓸 필요도 없고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 압축해서 읽기 어렵게 만들 필요도 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 장점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간결하고 한눈에 로직이 보임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼 존재 여부 + NaN 체크까지 한 줄에 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 단점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;df1['a'].isnull().any()는 &quot;하나라도 NaN이면 무조건 NaN 반환&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 즉, 값이 섞여 있어도 무시함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예: [1, 2, NaN]이면 결과는 np.nan (❌ 실무에서 손해날 수 있음)&lt;/p&gt;
&lt;pre id=&quot;code_1753455327871&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# &quot;전체에 null이 하나라도 있으면 np.nan, 아니면 최대값&quot;
if 'a' in df1.columns:
    df['a'] = np.nan if df1['a'].isnull().any() else df1['a'].max()
else:
    df['a'] = np.nan&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 버전 1 &amp;mdash; 간결한 3항 연산자 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 장점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훨씬 정확하고 실무 친화적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;df1['a'] = [NaN, NaN, 5] 같은 경우 &amp;rarr; 5를 반환 (정상)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;df1['a'] = [NaN, NaN, NaN] &amp;rarr; np.nan (정상)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 단점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 약간 더 길어짐 (하지만 논리 분기상 더 명확)&lt;/p&gt;
&lt;pre id=&quot;code_1753478160200&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if 'a' in df1.columns:
    if df1['a'].notna().any():  # NaN이 아닌 값이 하나라도 있을 때
        df['a'] = df1['a'].max()
    else:  # 모든 값이 NaN
        df['a'] = np.nan
else:  # 컬럼 자체 없음
    df['a'] = np.nan&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1753475687476&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if 'a' in df1.columns:
    if df1['a'].isnull().any():
        df['a'] = np.nan
    else:
        df['a'] = df1['a'].max()
else:
    df['a'] = np.nan&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1753475174259&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df['a'] = df1['a'].max() if 'a' in df1.columns and not df1['a'].isnull().any() else np.nan&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1753475460524&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df['a'] = df1['a'].max() if 'a' in df1 and not df1['a'].isnull().any() else np.nan&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1753451933258&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 개발자가 헷갈린 부분들:1. agg() 사용법 오해
# 잘못된 이해: agg()가 각 행에 함수를 적용한다고 생각
df1['a'].agg(lambda x: ...)  # 실제로는 전체 시리즈에 한 번만 적용

# 실제 용도: 그룹별이나 다중 집계용
df.groupby('group')['a'].agg(lambda x: ...)  # 이게 맞는 용법2. reset_index(drop=True) 오남용# 스칼라 결과에 reset_index 적용 (의미없음)
single_value.reset_index(drop=True)  # 스칼라엔 인덱스가 없음

# 실제 용도: DataFrame/Series 인덱스 재설정용
df.reset_index(drop=True)  # 이게 맞는 용법3. 브로드캐스팅 개념 부족# 스칼라를 DataFrame 컬럼에 할당하는 올바른 방법을 몰랐음
df['a'] = single_value  # pandas가 자동으로 브로드캐스팅해줌
# 또는
df['a'] = np.full(len(df), single_value) 
# 명시적 방법아마 다른 언어나 라이브러리 경험에서 오는 혼동이거나, 
# pandas 메서드들의 정확한 동작 방식을 제대로 학습하지 않고 &quot;되겠지&quot; 하는 마음으로 조합한 것 같네요.
# 이런 실수는 pandas 초보자들에게 꽤 흔한 패턴입니다!  &lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1087</guid>
      <comments>https://ljj777.tistory.com/1087#entry1087comment</comments>
      <pubDate>Fri, 25 Jul 2025 22:54:24 +0900</pubDate>
    </item>
    <item>
      <title>길이 불일치</title>
      <link>https://ljj777.tistory.com/1086</link>
      <description>&lt;pre id=&quot;code_1753450304761&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df['a'] = df1['a'].agg(lambda x: np.nan if x.isnull.any() else x.max()).reset_index(drop=True)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Colums must be same length as key 에러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 코드 `df['a'] = df1['a'].agg(...)`에서 발생한 에러는 `agg()`가 단일 값을 반환하기 때문에 `df['a']`의 길이와 맞지 않아서 발생한 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정&lt;/p&gt;
&lt;pre id=&quot;code_1753443601799&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd
import numpy as np

# 1. 예제 데이터 생성
df = pd.DataFrame({'other_col': [10, 20, 30]})  # 타겟 DataFrame (3행)
df1 = pd.DataFrame({'a': [1, np.nan, 3]})      # 소스 DataFrame (NaN 포함, 3행)

# 2. 조건에 따라 채울 값 계산
fill_value = np.nan if df1['a'].isnull().any() else df1['a'].max()

# 3. 전체 행에 동일 값 할당
df['a'] = np.full(len(df), fill_value)

# 4. 결과 출력
print(&quot;=== 최종 결과 ===&quot;)
print(df)
print(&quot;\n=== 검증 ===&quot;)
print(f&quot;df['a'] 길이: {len(df['a'])}, df 행 개수: {len(df)}&quot;)
print(f&quot;할당된 값: {fill_value}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드의 핵심 원리는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 코드에서 발생한 &quot;Columns must be same length as key&quot; 에러의 원인은 다음과 같습니다:&lt;br /&gt;&lt;br /&gt;```python&lt;br /&gt;df['a']&amp;nbsp;=&amp;nbsp;df1['a'].agg(lambda&amp;nbsp;x:&amp;nbsp;np.nan&amp;nbsp;if&amp;nbsp;x.isnull.any()&amp;nbsp;else&amp;nbsp;x.max()).reset_index(drop=True)&lt;br /&gt;```&lt;br /&gt;&lt;br /&gt;이&amp;nbsp;코드의&amp;nbsp;문제점들:&lt;br /&gt;&lt;br /&gt;1.&amp;nbsp;**`agg()`&amp;nbsp;결과가&amp;nbsp;스칼라**:&amp;nbsp;`agg()`는&amp;nbsp;전체&amp;nbsp;시리즈에&amp;nbsp;대해&amp;nbsp;하나의&amp;nbsp;집계값(스칼라)을&amp;nbsp;반환합니다&lt;br /&gt;2.&amp;nbsp;**`reset_index(drop=True)`의&amp;nbsp;무의미함**:&amp;nbsp;스칼라&amp;nbsp;값에는&amp;nbsp;인덱스가&amp;nbsp;없어서&amp;nbsp;이&amp;nbsp;메서드가&amp;nbsp;의미없습니다&lt;br /&gt;3.&amp;nbsp;**길이&amp;nbsp;불일치**:&amp;nbsp;스칼라&amp;nbsp;값을&amp;nbsp;DataFrame의&amp;nbsp;컬럼(여러&amp;nbsp;행)에&amp;nbsp;할당하려고&amp;nbsp;해서&amp;nbsp;길이가&amp;nbsp;맞지&amp;nbsp;않습니다&lt;br /&gt;&lt;br /&gt;**수정된&amp;nbsp;코드의&amp;nbsp;장점:**&lt;br /&gt;&lt;br /&gt;```python&lt;br /&gt;#&amp;nbsp;조건부&amp;nbsp;값&amp;nbsp;계산&amp;nbsp;(명확하고&amp;nbsp;읽기&amp;nbsp;쉬움)&lt;br /&gt;fill_value&amp;nbsp;=&amp;nbsp;np.nan&amp;nbsp;if&amp;nbsp;df1['a'].isnull().any()&amp;nbsp;else&amp;nbsp;df1['a'].max()&lt;br /&gt;&lt;br /&gt;#&amp;nbsp;전체&amp;nbsp;행에&amp;nbsp;동일한&amp;nbsp;값으로&amp;nbsp;채우기&lt;br /&gt;df['a']&amp;nbsp;=&amp;nbsp;np.full(len(df),&amp;nbsp;fill_value)&lt;br /&gt;```&lt;br /&gt;&lt;br /&gt;1.&amp;nbsp;**명확한&amp;nbsp;로직**:&amp;nbsp;조건&amp;nbsp;확인과&amp;nbsp;값&amp;nbsp;할당을&amp;nbsp;분리&lt;br /&gt;2.&amp;nbsp;**효율적**:&amp;nbsp;`np.full()`로&amp;nbsp;한&amp;nbsp;번에&amp;nbsp;모든&amp;nbsp;행을&amp;nbsp;같은&amp;nbsp;값으로&amp;nbsp;채움&lt;br /&gt;3.&amp;nbsp;**에러&amp;nbsp;없음**:&amp;nbsp;길이가&amp;nbsp;정확히&amp;nbsp;맞춰짐&lt;br /&gt;&lt;br /&gt;이런&amp;nbsp;식으로&amp;nbsp;집계&amp;nbsp;결과를&amp;nbsp;DataFrame&amp;nbsp;전체에&amp;nbsp;브로드캐스팅할&amp;nbsp;때는&amp;nbsp;스칼라&amp;nbsp;값을&amp;nbsp;명시적으로&amp;nbsp;처리하는&amp;nbsp;것이&amp;nbsp;가장&amp;nbsp;안전하고&amp;nbsp;명확한&amp;nbsp;방법입니다.&lt;/p&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1086</guid>
      <comments>https://ljj777.tistory.com/1086#entry1086comment</comments>
      <pubDate>Fri, 25 Jul 2025 16:33:03 +0900</pubDate>
    </item>
    <item>
      <title>카테고리타입</title>
      <link>https://ljj777.tistory.com/1085</link>
      <description>&lt;pre id=&quot;code_1753404724315&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd

def add_blank_category_or_fillna(series):
    try:
        if pd.api.types.is_categorical_dtype(series):
            # ''가 이미 포함되어 있는지 확인
            if '' not in series.cat.categories:
                return series.cat.add_categories([''])
            else:
                return series
        else:
            return series.fillna('')
    except Exception as e:
        print(f&quot;예외 발생: {e}&quot;)
        return series&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1753404746915&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# category 타입
s1 = pd.Series(['a', 'b', None], dtype='category')
s1 = add_blank_category_or_fillna(s1)
print(s1.cat.categories)  # ['a', 'b', '']

# object 타입
s2 = pd.Series(['x', None, 'y'])  # dtype = object
s2 = add_blank_category_or_fillna(s2)
print(s2)  # NaN &amp;rarr; ''로 대체됨&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1085</guid>
      <comments>https://ljj777.tistory.com/1085#entry1085comment</comments>
      <pubDate>Fri, 25 Jul 2025 09:53:01 +0900</pubDate>
    </item>
    <item>
      <title>Python으로 CSV &amp;rarr; DuckDB 저장</title>
      <link>https://ljj777.tistory.com/1084</link>
      <description>&lt;pre id=&quot;code_1752630371420&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import duckdb

# DuckDB DB 파일에 연결 (없으면 생성됨)
con = duckdb.connect(&quot;mydata.duckdb&quot;)

# CSV 파일 읽어서 테이블로 저장
con.execute(&quot;&quot;&quot;
CREATE TABLE my_table AS
SELECT * FROM read_csv_auto('sample.csv')
&quot;&quot;&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1752630618315&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd
import duckdb

df = pd.read_csv(&quot;sample.csv&quot;)

# DuckDB에 저장
con = duckdb.connect(&quot;mydata.duckdb&quot;)
con.register(&quot;df_view&quot;, df)

# DataFrame을 테이블로 저장
con.execute(&quot;CREATE TABLE my_table AS SELECT * FROM df_view&quot;)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1084</guid>
      <comments>https://ljj777.tistory.com/1084#entry1084comment</comments>
      <pubDate>Wed, 16 Jul 2025 10:46:14 +0900</pubDate>
    </item>
    <item>
      <title>error</title>
      <link>https://ljj777.tistory.com/1083</link>
      <description>&lt;pre id=&quot;code_1752553357993&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import traceback

try:
    # 에러 유발 코드 (예시: 0으로 나누기)
    x = 1 / 0
except Exception as e:
    print(&quot;❗ 에러 발생:&quot;, str(e))
    traceback.print_exc()  # 전체 에러 트레이스 출력&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1752554708260&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import numpy as np

# 인덱스가 다른 위치 찾기
diff_indices = np.where(df.index != df1.index)[0]

# 결과 출력
print(&quot;인덱스가 다른 위치:&quot;, diff_indices)
print(&quot;\n--- df의 인덱스 ---&quot;)
print(df.index[diff_indices])
print(&quot;\n--- df1의 인덱스 ---&quot;)
print(df1.index[diff_indices])&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1083</guid>
      <comments>https://ljj777.tistory.com/1083#entry1083comment</comments>
      <pubDate>Tue, 15 Jul 2025 13:22:55 +0900</pubDate>
    </item>
    <item>
      <title>날짜결측치처리</title>
      <link>https://ljj777.tistory.com/1082</link>
      <description>&lt;pre id=&quot;code_1752104850167&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;first_date = df.get('날짜열', pd.Series([pd.NaT])).iloc[0]&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1082</guid>
      <comments>https://ljj777.tistory.com/1082#entry1082comment</comments>
      <pubDate>Thu, 10 Jul 2025 08:48:02 +0900</pubDate>
    </item>
    <item>
      <title>pandas concat</title>
      <link>https://ljj777.tistory.com/1081</link>
      <description>&lt;pre id=&quot;code_1752025042755&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd

# 예시: 둘 다 빈 DataFrame이지만 컬럼이 다름
df1 = pd.DataFrame(columns=['a', 'b'])
df2 = pd.DataFrame(columns=['b', 'c'])

#   모든 컬럼의 합집합 구하기
all_columns = sorted(set(df1.columns).union(set(df2.columns)))

#   컬럼 맞춰주기 (reindex로 없으면 NaN 채움)
df1 = df1.reindex(columns=all_columns)
df2 = df2.reindex(columns=all_columns)

#   concat
result = pd.concat([df1, df2], ignore_index=True)

print(result)&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1081</guid>
      <comments>https://ljj777.tistory.com/1081#entry1081comment</comments>
      <pubDate>Wed, 9 Jul 2025 10:37:39 +0900</pubDate>
    </item>
    <item>
      <title>문자열 datetime</title>
      <link>https://ljj777.tistory.com/1080</link>
      <description>&lt;pre id=&quot;code_1751951242042&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd

df = pd.DataFrame({
    'a': ['2024-01-01', '2024-01-02', None]
})

# 문자열 &amp;rarr; datetime으로 변환
df['a'] = pd.to_datetime(df['a'], errors='coerce')

# 첫 번째 값 가져오기
first_val = df['a'].iloc[0]  # 또는 .iat[0]

print(first_val)&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1080</guid>
      <comments>https://ljj777.tistory.com/1080#entry1080comment</comments>
      <pubDate>Tue, 8 Jul 2025 14:07:42 +0900</pubDate>
    </item>
    <item>
      <title>API 요청을 통해 res를 받아오고, 그 안에 &amp;quot;signal&amp;quot; 값을 추출</title>
      <link>https://ljj777.tistory.com/1079</link>
      <description>&lt;pre id=&quot;code_1751460842886&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import requests

url = &quot;https://your-api-url.com/stock/signal&quot;
params = {
    &quot;stock_name&quot;: &quot;극동유화&quot;,
    &quot;max_news&quot;: 50
}
headers = {
    &quot;Authorization&quot;: &quot;Bearer YOUR_API_KEY&quot;  # 필요 시
}

response = requests.post(url, json=params, headers=headers)

# 응답값이 JSON이면
if response.status_code == 200:
    res = response.json()
    signal = res.get(&quot;signal&quot;)  # 또는 res[&quot;signal&quot;]
    print(&quot;  Signal:&quot;, signal)
else:
    print(&quot;❌ API 호출 실패:&quot;, response.status_code, response.text)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  출력 예시:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Signal: buy&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  추가 팁:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;res.get(&quot;signal&quot;)을 사용하면 &quot;signal&quot; 키가 없을 때도 에러가 나지 않고 None을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1079</guid>
      <comments>https://ljj777.tistory.com/1079#entry1079comment</comments>
      <pubDate>Wed, 2 Jul 2025 21:54:43 +0900</pubDate>
    </item>
    <item>
      <title>컬럼 값 비교</title>
      <link>https://ljj777.tistory.com/1072</link>
      <description>&lt;pre id=&quot;code_1750995727224&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd
from openpyxl import load_workbook
from openpyxl.styles import PatternFill

#   파일 경로
file_path = '원본.xlsx'

# 1. pandas로 데이터 읽기
df = pd.read_excel(file_path)

# 기준 데이터 (a, b)
left = df[['a', 'b']].dropna(subset=['a']).copy()
left['a_clean'] = left['a'].astype(str).str.strip().str.lower()
left['b_clean'] = left['b'].astype(str).str.strip().str.lower()

# 비교 대상 데이터 (c, d)
right = df[['c', 'd']].dropna(subset=['c']).copy()
right['c_clean'] = right['c'].astype(str).str.strip().str.lower()
right['d_clean'] = right['d'].astype(str).str.strip().str.lower()

# 2. 키 기준으로 내부 조인 (모든 조합 비교)
merged = pd.merge(left, right, left_on='a_clean', right_on='c_clean', how='inner')

# 3. 비교 결과
# key: c_clean
# 값이 일치하는 c_clean은 흰색, 불일치하는 c_clean은 노란색 대상
color_map = {}  # key = (c값, d값), value = 'white' or 'yellow'

for _, row in merged.iterrows():
    key = (row['c'], row['d'])  # 실제 표시용 키
    if row['b_clean'] == row['d_clean']:
        color_map[key] = 'white'
    else:
        color_map[key] = 'yellow'

# 4. openpyxl 로드
wb = load_workbook(file_path)
ws = wb.active

# 5. 색상 정의
fill_yellow = PatternFill(start_color=&quot;FFFF00&quot;, end_color=&quot;FFFF00&quot;, fill_type=&quot;solid&quot;)
fill_none = PatternFill(fill_type=None)  # 색 제거용

# 6. C/D 열에서 색칠
for row in range(2, ws.max_row + 1):
    c_val = ws[f&quot;C{row}&quot;].value
    d_val = ws[f&quot;D{row}&quot;].value
    key = (c_val, d_val)

    if key in color_map:
        if color_map[key] == 'yellow':
            ws[f&quot;D{row}&quot;].fill = fill_yellow
        else:
            ws[f&quot;D{row}&quot;].fill = fill_none  # 흰색 처리

# 7. 저장
wb.save(file_path)
print(&quot;✅ 완료: 중복 키 처리 포함해 D 셀 색칠/색 제거 완료&quot;)&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1072</guid>
      <comments>https://ljj777.tistory.com/1072#entry1072comment</comments>
      <pubDate>Fri, 27 Jun 2025 12:12:02 +0900</pubDate>
    </item>
    <item>
      <title>안전하게 삭제하는 코드 예시</title>
      <link>https://ljj777.tistory.com/1070</link>
      <description>&lt;pre id=&quot;code_1750977765034&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import os
import time
from openpyxl import load_workbook

temp_path = &quot;temp.xlsx&quot;

try:
    wb = load_workbook(temp_path)
    ws = wb.active
    # ... 작업 ...

finally:
    # 임시 파일 닫고 삭제 (삭제 실패 시 재시도)
    try:
        wb.close()
    except:
        pass

    max_retries = 5
    retry_delay = 0.5  # 초
    for attempt in range(1, max_retries + 1):
        try:
            os.remove(temp_path)
            print(f&quot;✅ 임시 파일 삭제 완료: {temp_path}&quot;)
            break
        except PermissionError as e:
            print(f&quot;⚠️ 삭제 실패 (시도 {attempt}/{max_retries}) - 파일이 열려 있거나 사용 중&quot;)
            time.sleep(retry_delay)
        except Exception as e:
            print(f&quot;❗ 알 수 없는 오류: {e}&quot;)
            break
    else:
        print(f&quot;❌ 최종 실패: 파일이 계속 사용 중입니다. 직접 닫은 후 수동 삭제하세요 &amp;rarr; {temp_path}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법&amp;nbsp;1:&amp;nbsp;wb.close()&amp;nbsp;후&amp;nbsp;삭제&amp;nbsp;(가장&amp;nbsp;확실함)&lt;/p&gt;
&lt;pre id=&quot;code_1750933455277&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from openpyxl import load_workbook
import os
import time
import gc

temp_path = &quot;temp.xlsx&quot;

# temp.xlsx를 열어 처리
wb = load_workbook(temp_path)
ws = wb.active

# ... 원하는 작업 수행 ...

# 반드시 닫기
wb.close()
del wb
gc.collect()  # 가비지 컬렉션으로 완전 해제

# 잠시 대기 후 삭제 시도 (잠금 해제 지연 방지)
time.sleep(0.5)

# 삭제 시도
try:
    os.remove(temp_path)
    print(&quot;임시 파일 삭제 완료&quot;)
except PermissionError:
    print(&quot;❗ 파일이 열려 있거나 사용 중입니다. 수동으로 닫은 후 다시 시도하세요.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750930658752&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import os
import time
import gc

file_path = &quot;경로/파일.xlsx&quot;

# 파일 작업 이후 객체 해제
try:
    wb.save(file_path)
    wb.close()  # 명시적으로 닫기
    del wb
    gc.collect()  # 가비지 컬렉션으로 파일 잠금 해제 유도
except Exception as e:
    print(&quot;파일 저장 오류:&quot;, e)

# 삭제 시도 (엑셀이 열려 있으면 실패)
try:
    os.remove(file_path)
    print(&quot;삭제 성공&quot;)
except PermissionError:
    print(&quot;⚠️ 파일이 열려 있어서 삭제할 수 없습니다. 엑셀 창을 닫고 다시 시도하세요.&quot;)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/python</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1070</guid>
      <comments>https://ljj777.tistory.com/1070#entry1070comment</comments>
      <pubDate>Thu, 26 Jun 2025 18:38:24 +0900</pubDate>
    </item>
    <item>
      <title>git merge</title>
      <link>https://ljj777.tistory.com/1069</link>
      <description>&lt;pre id=&quot;code_1750913770557&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. 현재 어떤 브랜치에 있는지 확인
git branch

# 2. main 브랜치로 이동
git checkout main

# 3. 최신 상태로 업데이트 (필요 시)
git pull origin main

# 4. a 브랜치를 main에 머지
git merge a

# 5. 원격 저장소에 반영
git push origin main&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/python</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1069</guid>
      <comments>https://ljj777.tistory.com/1069#entry1069comment</comments>
      <pubDate>Thu, 26 Jun 2025 13:56:50 +0900</pubDate>
    </item>
    <item>
      <title>None type replace</title>
      <link>https://ljj777.tistory.com/1068</link>
      <description>&lt;pre id=&quot;code_1750654132601&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if text is not None:
    text = text.replace(&quot;a&quot;, &quot;b&quot;)
else:
    text = &quot;&quot;  # 또는 기본값 설정&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1068</guid>
      <comments>https://ljj777.tistory.com/1068#entry1068comment</comments>
      <pubDate>Mon, 23 Jun 2025 13:49:17 +0900</pubDate>
    </item>
    <item>
      <title>화면보호기</title>
      <link>https://ljj777.tistory.com/1066</link>
      <description>&lt;pre id=&quot;code_1750137987634&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pynput import keyboard, mouse
from pynput.keyboard import Controller, Key
import threading
import time
from datetime import datetime
import ctypes
import os

# ✅ 로그 저장 기본 디렉토리 (원하는 경로로 변경하세요)
LOG_BASE_DIR = r&quot;D:/logs&quot;

# 설정
IDLE_TIME_LIMIT = 300  # 5분 이상 비활동 시
CHECK_INTERVAL = 10    # 활동 체크 주기
keyboard_controller = Controller()
last_activity_time = time.time()

# 로그 기록 함수
def write_log(message):
    now = datetime.now()
    timestamp = now.strftime('%Y-%m-%d %H:%M:%S')
    year = now.strftime('%Y')
    month = now.strftime('%m')
    day = now.strftime('%d')

    log_dir = os.path.join(LOG_BASE_DIR, year, month)
    os.makedirs(log_dir, exist_ok=True)  # 폴더 없으면 생성

    log_path = os.path.join(log_dir, f&quot;{day}.txt&quot;)
    log_line = f&quot;[{timestamp}] {message}&quot;

    print(log_line)
    with open(log_path, &quot;a&quot;, encoding=&quot;utf-8&quot;) as f:
        f.write(log_line + &quot;\n&quot;)

# 사용자 입력 감지 핸들러
def on_input_activity(event):
    global last_activity_time
    last_activity_time = time.time()

# 화면보호기 방지를 위한 Shift 입력
def prevent_sleep_with_key():
    write_log(&quot;  활동 없음: Shift 입력 시뮬레이션&quot;)
    keyboard_controller.press(Key.shift)
    time.sleep(0.1)
    keyboard_controller.release(Key.shift)

# 화면 잠금
def lock_workstation():
    write_log(&quot;  화면 잠금 실행&quot;)
    ctypes.windll.user32.LockWorkStation()

# 시스템 종료
def shutdown_system():
    write_log(&quot;⏹️ 시스템 종료 명령 실행&quot;)
    os.system(&quot;shutdown /s /t 0&quot;)

# 시스템 재부팅
def reboot_system():
    write_log(&quot;  시스템 재부팅 명령 실행&quot;)
    os.system(&quot;shutdown /r /t 0&quot;)

# 활동 감지 쓰레드
def monitor_idle():
    while True:
        idle_time = time.time() - last_activity_time
        if idle_time &amp;gt;= IDLE_TIME_LIMIT:
            prevent_sleep_with_key()
        time.sleep(CHECK_INTERVAL)

# 시간 기반 이벤트 실행
def scheduled_actions():
    triggered = set()
    while True:
        now = datetime.now()
        current_time = now.strftime('%H:%M')
        weekday = now.weekday()  # 0:월 ~ 4:금

        # 오전 11시 화면 잠금
        if current_time == &quot;11:00&quot; and &quot;lock&quot; not in triggered:
            lock_workstation()
            triggered.add(&quot;lock&quot;)

        # 오후 5시 종료/재부팅
        if current_time == &quot;17:00&quot; and &quot;shutdown&quot; not in triggered:
            if weekday in [0, 1, 2, 3]:  # 월~목
                write_log(&quot;  월~목: 시스템 재부팅 예정&quot;)
                reboot_system()
            elif weekday == 4:  # 금
                write_log(&quot;⏹️ 금요일: 시스템 종료 예정&quot;)
                shutdown_system()
            triggered.add(&quot;shutdown&quot;)

        if current_time == &quot;00:00&quot;:
            triggered.clear()

        time.sleep(5)

# 입력 리스너 시작
keyboard.Listener(on_press=on_input_activity).start()
mouse.Listener(
    on_move=on_input_activity,
    on_click=on_input_activity,
    on_scroll=on_input_activity
).start()

# 쓰레드 실행
threading.Thread(target=monitor_idle, daemon=True).start()
threading.Thread(target=scheduled_actions, daemon=True).start()

write_log(&quot;✅ 활동 감지 + 자동 잠금/재부팅/종료 프로그램 시작됨 (Ctrl+C로 종료 가능)&quot;)

try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    write_log(&quot;  프로그램 수동 종료됨&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1749798316547&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pyinstaller --noconsole --onefile idle_guard.py&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1066</guid>
      <comments>https://ljj777.tistory.com/1066#entry1066comment</comments>
      <pubDate>Fri, 13 Jun 2025 08:12:56 +0900</pubDate>
    </item>
    <item>
      <title>cProfile</title>
      <link>https://ljj777.tistory.com/1065</link>
      <description>&lt;pre id=&quot;code_1749706648208&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;python -m cProfile -o profile_results.prof your_script.py&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1749706664323&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;python -m pstats profile_results.prof&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1065</guid>
      <comments>https://ljj777.tistory.com/1065#entry1065comment</comments>
      <pubDate>Thu, 12 Jun 2025 14:38:02 +0900</pubDate>
    </item>
    <item>
      <title>numba</title>
      <link>https://ljj777.tistory.com/1064</link>
      <description>&lt;pre id=&quot;code_1749703545871&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import numpy as np
import pandas as pd
from numba import njit

@njit
def numba_select(condlist, choicelist, default=0):
    output = np.full(condlist[0].shape, default)
    for cond, choice in zip(reversed(condlist), reversed(choicelist)):
        output = np.where(cond, choice, output)
    return output  # NumPy 배열 반환

# 예시 데이터
condlist = [np.array([True, False, False]), np.array([False, True, False])]
choicelist = [np.array([1, 1, 1]), np.array([2, 2, 2])]

# Numba 함수 실행
result_array = numba_select(condlist, choicelist, default=0)

# NumPy 배열 &amp;rarr; Pandas DataFrame 변환
df = pd.DataFrame({&quot;result&quot;: result_array})
print(df)&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1064</guid>
      <comments>https://ljj777.tistory.com/1064#entry1064comment</comments>
      <pubDate>Thu, 12 Jun 2025 10:47:18 +0900</pubDate>
    </item>
    <item>
      <title>np.select 멀티프로세싱 적용</title>
      <link>https://ljj777.tistory.com/1063</link>
      <description>&lt;pre id=&quot;code_1749684555183&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import numpy as np
import pandas as pd
import multiprocessing as mp
from functools import partial

def process_chunk(df_chunk, conditions, choices, default):
    # 각 청크에 np.select 적용
    result = np.select(conditions, choices, default=default)
    return pd.Series(result, index=df_chunk.index)

def parallel_select(df, conditions, choices, default='default', num_processes=None):
    if num_processes is None:
        num_processes = mp.cpu_count()
    
    # 데이터 분할
    chunks = np.array_split(df, num_processes)
    
    # 부분 함수 생성 (conditions, choices, default 고정)
    worker = partial(process_chunk, conditions=conditions, choices=choices, default=default)
    
    with mp.Pool(num_processes) as pool:
        results = pool.map(worker, chunks)
    
    # 결과 병합
    return pd.concat(results)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1749684584143&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 대용량 데이터 생성 (1000만 행)
df = pd.DataFrame({
    'col1': np.random.randint(0, 100, 10_000_000),
    'col2': np.random.choice(['A','B','C'], 10_000_000),
    'col3': np.random.randn(10_000_000)
})

# 복잡한 조건 정의
conditions = [
    (df['col1'] &amp;gt; 50) &amp;amp; (df['col2'] == 'A'),
    (df['col1'] &amp;lt; 20) | (df['col3'].abs() &amp;gt; 2),
    df['col2'].isin(['B','C'])
]

choices = ['High A', 'Low or Outlier', 'B or C']

# 멀티프로세싱 적용
df['category'] = parallel_select(df, conditions, choices, default='Other')&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1063</guid>
      <comments>https://ljj777.tistory.com/1063#entry1063comment</comments>
      <pubDate>Thu, 12 Jun 2025 08:30:19 +0900</pubDate>
    </item>
    <item>
      <title>메모리측정</title>
      <link>https://ljj777.tistory.com/1062</link>
      <description>&lt;pre id=&quot;code_1749612739583&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd

def report_memory_usage(df: pd.DataFrame, sort: bool = True, top: int = None) -&amp;gt; pd.DataFrame:
    &quot;&quot;&quot;
    DataFrame의 열별 실제 메모리 사용량을 GB 단위로 리포팅합니다.
    'Index'는 제외됩니다.

    Args:
        df (pd.DataFrame): 측정할 DataFrame
        sort (bool): 메모리 사용량 기준 정렬 여부 (default: True)
        top (int): 상위 N개 열만 출력 (default: None: 전체)

    Returns:
        pd.DataFrame: 열별 메모리 사용량(GByte), dtype 포함
    &quot;&quot;&quot;
    usage = df.memory_usage(deep=True) / 1024**3  # GB 단위
    usage = usage.drop('Index')  #   'Index' 항목 제거

    usage_df = pd.DataFrame({
        'column': usage.index,
        'memory_gb': usage.values,
        'dtype': [df[col].dtype for col in usage.index]
    })

    if sort:
        usage_df = usage_df.sort_values(by='memory_gb', ascending=False)

    if top:
        usage_df = usage_df.head(top)

    usage_df.reset_index(drop=True, inplace=True)
    total = usage_df['memory_gb'].sum()
    print(f&quot;  Total memory usage (columns only): {total:.4f} GB&quot;)

    return usage_df&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1749612278587&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df = pd.DataFrame({
    'id': range(10_000_000),
    'name': ['apple'] * 10_000_000,
    'value': [3.14] * 10_000_000
})

mem_report = report_memory_usage(df)
print(mem_report)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1749612293705&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  Total memory usage: 0.5584 GB

     column  memory_gb     dtype
0      name     0.3810    object
1        id     0.0763      int64
2     value     0.0763    float64&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1749613184350&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def report_memory_usage(df: pd.DataFrame, sort: bool = True, top: int = None, df_name: str = None) -&amp;gt; pd.DataFrame:
    &quot;&quot;&quot;
    DataFrame의 열별 실제 메모리 사용량을 GB 단위로 리포팅합니다.
    'Index'는 제외됩니다.

    Args:
        df (pd.DataFrame): 측정할 DataFrame
        sort (bool): 메모리 사용량 기준 정렬 여부 (default: True)
        top (int): 상위 N개 열만 출력 (default: None: 전체)
        df_name (str): DataFrame 이름 (default: None)

    Returns:
        pd.DataFrame: 열별 메모리 사용량(GByte), dtype 포함
    &quot;&quot;&quot;
    usage = df.memory_usage(deep=True) / 1024**3  # GB 단위
    usage = usage.drop('Index')  # 'Index' 항목 제거

    usage_df = pd.DataFrame({
        'column': usage.index,
        'memory_gb': usage.values,
        'dtype': [df[col].dtype for col in usage.index]
    })

    if sort:
        usage_df = usage_df.sort_values(by='memory_gb', ascending=False)

    if top:
        usage_df = usage_df.head(top)

    usage_df.reset_index(drop=True, inplace=True)
    total = usage_df['memory_gb'].sum()

    if df_name:
        print(f&quot;  Total memory usage of '{df_name}' (columns only): {total:.4f} GB&quot;)
    else:
        print(f&quot;  Total memory usage (columns only): {total:.4f} GB&quot;)

    return usage_df&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1062</guid>
      <comments>https://ljj777.tistory.com/1062#entry1062comment</comments>
      <pubDate>Wed, 11 Jun 2025 12:24:57 +0900</pubDate>
    </item>
    <item>
      <title>Type 변경</title>
      <link>https://ljj777.tistory.com/1061</link>
      <description>&lt;pre id=&quot;code_1749448329732&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cols = ['col1', 'col2', 'col3']  # category로 바꿀 컬럼 리스트
df[cols] = df[cols].apply(lambda x: x.astype('category'))&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1749449619659&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd
import numpy as np

# 예시 데이터 생성
df = pd.DataFrame({
    'col1': np.random.choice(['apple', 'banana', 'cherry'], 1_000_000),
    'col2': np.random.choice(['red', 'green', 'blue'], 1_000_000),
    'col3': np.random.choice(['small', 'medium', 'large'], 1_000_000)
})

#   메모리 사용량 (변환 전)
print(&quot;변환 전 메모리 사용량:&quot;)
print(df.memory_usage(deep=True))
print(&quot;총합:&quot;, df.memory_usage(deep=True).sum() / 1024**2, &quot;MB&quot;)

#   category 변환
df = df.astype({col: 'category' for col in ['col1', 'col2', 'col3']})

#   메모리 사용량 (변환 후)
print(&quot;\n변환 후 메모리 사용량:&quot;)
print(df.memory_usage(deep=True))
print(&quot;총합:&quot;, df.memory_usage(deep=True).sum() / 1024**2, &quot;MB&quot;)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1061</guid>
      <comments>https://ljj777.tistory.com/1061#entry1061comment</comments>
      <pubDate>Mon, 9 Jun 2025 14:52:24 +0900</pubDate>
    </item>
    <item>
      <title>join</title>
      <link>https://ljj777.tistory.com/1060</link>
      <description>&lt;pre id=&quot;code_1749009783649&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd

df1 = pd.DataFrame({'A': [1, 2], 'B': [3, 4]}, index=['x', 'y'])
df2 = pd.DataFrame({'A': [5, 6], 'C': [7, 8]}, index=['x', 'y'])

# Left join with drop=False
result = df1.join(df2.set_index('A', drop=False), 
                 how='left',  # 명시적으로 left join 지정
                 lsuffix='_left', 
                 rsuffix='_right')

print(result)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1060</guid>
      <comments>https://ljj777.tistory.com/1060#entry1060comment</comments>
      <pubDate>Mon, 2 Jun 2025 13:40:03 +0900</pubDate>
    </item>
    <item>
      <title>merge</title>
      <link>https://ljj777.tistory.com/1058</link>
      <description>&lt;pre id=&quot;code_1748477370842&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from concurrent.futures import ThreadPoolExecutor
import numpy as np
import pandas as pd

def safe_parallel_merge(df1, df2, left_key, right_key=None, n_partitions=4, how='left'):
    &quot;&quot;&quot;
    개선된 병렬 merge 함수 - 안정성 강화 버전
    
    Parameters:
    - df1: 왼쪽 DataFrame
    - df2: 오른쪽 DataFrame
    - left_key: df1의 조인 키 (컬럼명 또는 컬럼 리스트)
    - right_key: df2의 조인 키 (None이면 left_key와 동일)
    - n_partitions: 분할 개수
    - how: merge 방식 ('left', 'right', 'inner', 'outer')
    &quot;&quot;&quot;
    # 1. 키 컬럼 검증 및 표준화
    right_key = right_key if right_key is not None else left_key
    
    left_keys = [left_key] if isinstance(left_key, str) else list(left_key)
    right_keys = [right_key] if isinstance(right_key, str) else list(right_key)
    
    # 2. 키 컬럼 존재 여부 확인
    missing_in_left = set(left_keys) - set(df1.columns)
    missing_in_right = set(right_keys) - set(df2.columns)
    
    if missing_in_left:
        raise ValueError(f&quot;df1에 다음 키 컬럼이 없습니다: {missing_in_left}&quot;)
    if missing_in_right:
        raise ValueError(f&quot;df2에 다음 키 컬럼이 없습니다: {missing_in_right}&quot;)
    
    # 3. 키 컬럼 타입 통일 (중요!)
    for lk, rk in zip(left_keys, right_keys):
        df1[lk] = df1[lk].astype(df2[rk].dtype)
    
    # 4. 안정적인 데이터 분할
    df1 = df1.reset_index(drop=True)
    split_indices = np.linspace(0, len(df1), n_partitions + 1, dtype=int)
    chunks = [df1.iloc[split_indices[i]:split_indices[i+1]] for i in range(n_partitions)]
    
    # 5. 병렬 처리
    results = []
    with ThreadPoolExecutor(max_workers=n_partitions) as executor:
        futures = []
        for chunk in chunks:
            futures.append(
                executor.submit(
                    pd.merge,
                    chunk.copy(),  # 안정성을 위해 복사본 사용
                    df2.copy(),
                    left_on=left_keys,
                    right_on=right_keys,
                    how=how
                )
            )
        
        for future in futures:
            try:
                result = future.result()
                results.append(result)
            except Exception as e:
                executor.shutdown(wait=False)
                raise RuntimeError(f&quot;병렬 merge 실패: {str(e)}&quot;)
    
    # 6. 결과 병합 및 중복 처리
    final_df = pd.concat(results, ignore_index=True)
    
    if how in ['outer', 'right']:
        final_df = final_df.drop_duplicates(subset=left_keys if how == 'right' else right_keys)
    
    return final_df&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748416543228&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 같은 키 이름
result = safe_parallel_merge(df1, df2, left_key='id', n_partitions=4)

# 다른 키 이름
result = safe_parallel_merge(df1, df2, left_key='df1_id', right_key='df2_id')&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748416571439&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 여러 컬럼으로 조인
result = safe_parallel_merge(
    df1, df2, 
    left_key=['date', 'user_id'],
    right_key=['transaction_date', 'customer_id']
)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1058</guid>
      <comments>https://ljj777.tistory.com/1058#entry1058comment</comments>
      <pubDate>Wed, 28 May 2025 16:04:23 +0900</pubDate>
    </item>
    <item>
      <title>Polars type 변경</title>
      <link>https://ljj777.tistory.com/1057</link>
      <description>&lt;pre id=&quot;code_1748235772638&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import polars as pl

def process_dataframe_optimized_pl(dict_df_types: dict, df: pl.DataFrame) -&amp;gt; pl.DataFrame:
    def handle_column(col: str, dtype: str) -&amp;gt; pl.Expr:
        try:
            expr = pl.col(col)

            if dtype == 'int':
                return expr.cast(pl.Int32).fill_null(0).alias(col)
            elif dtype == 'float':
                return expr.cast(pl.Float32).fill_null(0).alias(col)
            elif dtype == 'bool':
                return expr.cast(pl.Utf8).str.to_lowercase().is_in(['true', 't', '1']).alias(col)
            elif dtype == 'datetime':
                return expr.cast(pl.Utf8).str.strptime(pl.Datetime, strict=False).alias(col)
            elif dtype == 'string':
                return expr.cast(pl.String).alias(col)
            elif dtype == 'category':
                return expr.cast(pl.Categorical).alias(col)
            else:
                return expr  # return original if dtype not recognized
        except Exception as e:
            print(f&quot;컬럼 '{col}' 처리 중 오류 발생: {e}&quot;)
            return expr  # return original on error

    # Get intersection of DataFrame columns and dictionary keys
    valid_cols = set(df.columns) &amp;amp; set(dict_df_types.keys())
    
    # Filter for only valid types we want to process
    valid_types = {'int', 'float', 'bool', 'datetime', 'string', 'category'}
    
    # Create expressions for columns that need processing
    exprs = [
        handle_column(col, dict_df_types[col])
        for col in valid_cols
        if dict_df_types.get(col) in valid_types
    ]
    
    return df.with_columns(exprs)&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1057</guid>
      <comments>https://ljj777.tistory.com/1057#entry1057comment</comments>
      <pubDate>Mon, 26 May 2025 13:49:16 +0900</pubDate>
    </item>
    <item>
      <title>object 타입 컬럼을 모두 문자열(str)로 변환</title>
      <link>https://ljj777.tistory.com/1056</link>
      <description>&lt;pre id=&quot;code_1748230337452&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd

def convert_object_columns_to_str(df: pd.DataFrame) -&amp;gt; pd.DataFrame:
    &quot;&quot;&quot;
    Pandas DataFrame에서 object 타입 컬럼을 모두 문자열(str)로 변환합니다.
    
    Parameters:
        df (pd.DataFrame): 변환할 DataFrame
    
    Returns:
        pd.DataFrame: 문자열 컬럼이 str 타입으로 변환된 DataFrame
    &quot;&quot;&quot;
    df = df.copy()
    for col in df.select_dtypes(include='object').columns:
        df[col] = df[col].astype(str)
    return df&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748230351376&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import polars as pl

pandas_df = pd.read_csv(&quot;data.csv&quot;)
pandas_df = convert_object_columns_to_str(pandas_df)
lazy_df = pl.from_pandas(pandas_df).lazy()&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1056</guid>
      <comments>https://ljj777.tistory.com/1056#entry1056comment</comments>
      <pubDate>Mon, 26 May 2025 12:33:00 +0900</pubDate>
    </item>
    <item>
      <title>df count</title>
      <link>https://ljj777.tistory.com/1055</link>
      <description>&lt;pre id=&quot;code_1747291118796&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd

# 샘플 DataFrame 생성
df = pd.DataFrame({'A': [1, 2, None, 4], 'B': ['x', None, 'z', 'w']})

# 1. len() 함수 사용 (가장 일반적)
row_count = len(df)
print(f&quot;행 수 (len): {row_count}&quot;)

# 2. shape 속성 사용
row_count = df.shape[0]  # shape는 (행수, 열수) 튜플 반환
print(f&quot;행 수 (shape): {row_count}&quot;)

# 3. index 길이 확인
row_count = df.index.size
print(f&quot;행 수 (index): {row_count}&quot;)

# 4. 각 열별 결측값(None/NaN) 개수 확인
print(&quot;\n각 열별 결측값 개수:&quot;)
print(df.isnull().sum())  # 또는 df.isna().sum()

# 5. 전체 결측값 개수 확인
total_nulls = df.isnull().sum().sum()
print(f&quot;\n전체 결측값 총 개수: {total_nulls}&quot;)

# 6. 결측값이 있는 행만 카운트
null_rows_count = df.isnull().any(axis=1).sum()
print(f&quot;결측값이 하나라도 있는 행 수: {null_rows_count}&quot;)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1055</guid>
      <comments>https://ljj777.tistory.com/1055#entry1055comment</comments>
      <pubDate>Thu, 15 May 2025 15:38:54 +0900</pubDate>
    </item>
    <item>
      <title>oracle Procedure 내용조회</title>
      <link>https://ljj777.tistory.com/1054</link>
      <description>&lt;pre id=&quot;code_1746603326718&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 특정 프로시저 내용 확인
SELECT text 
FROM all_source 
WHERE name = '프로시저명' 
AND type = 'PROCEDURE' 
ORDER BY line;

-- 모든 프로시저 목록
SELECT object_name 
FROM all_objects 
WHERE object_type = 'PROCEDURE';&lt;/code&gt;&lt;/pre&gt;</description>
      <category>데이타베이스</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1054</guid>
      <comments>https://ljj777.tistory.com/1054#entry1054comment</comments>
      <pubDate>Wed, 7 May 2025 16:35:29 +0900</pubDate>
    </item>
    <item>
      <title>macOS 기준 Android Studio 완전 삭제 방법</title>
      <link>https://ljj777.tistory.com/1053</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.   앱 제거&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1746276349855&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo rm -rf /Applications/Android\ Studio.app&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.   설정 및 캐시 제거&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1746276377588&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rm -rf ~/Library/Application\ Support/Google/AndroidStudio*
rm -rf ~/Library/Preferences/AndroidStudio*
rm -rf ~/Library/Logs/AndroidStudio*
rm -rf ~/Library/Caches/AndroidStudio*
rm -rf ~/.android
rm -rf ~/.gradle
rm -rf ~/Library/Android&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 삭제 방법 (macOS) : &lt;/b&gt;&lt;b&gt;(선택) SDK도 삭제할 경우&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1746276476815&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rm -rf ~/Library/Android/sdk&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  전체 삭제 명령어 (복사해서 터미널에 붙여 넣기)&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1746276612576&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Android Studio 앱 삭제
sudo rm -rf /Applications/Android\ Studio.app

# 설정 및 캐시 제거
rm -rf ~/Library/Preferences/AndroidStudio*
rm -rf ~/Library/Application\ Support/Google/AndroidStudio*
rm -rf ~/Library/Caches/AndroidStudio*
rm -rf ~/Library/Logs/AndroidStudio*

# Android SDK, AVD, Gradle 등 관련 구성 제거
rm -rf ~/Library/Android
rm -rf ~/.android
rm -rf ~/.gradle&lt;/code&gt;&lt;/pre&gt;</description>
      <category>서버/MAC</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1053</guid>
      <comments>https://ljj777.tistory.com/1053#entry1053comment</comments>
      <pubDate>Sat, 3 May 2025 21:46:21 +0900</pubDate>
    </item>
    <item>
      <title>csv reader</title>
      <link>https://ljj777.tistory.com/1052</link>
      <description>&lt;pre id=&quot;code_1745987817789&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
import os
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import json
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QTableView, QFileDialog,
    QVBoxLayout, QWidget, QPushButton, QLabel,
    QStatusBar, QMessageBox, QLineEdit, QHBoxLayout,
    QComboBox, QHeaderView, QProgressDialog
)
from PyQt5.QtCore import (
    Qt, QAbstractTableModel, QSortFilterProxyModel, 
    QThread, pyqtSignal, QObject
)

class LoadWorker(QThread):
    &quot;&quot;&quot;데이터 로딩 작업 스레드 (Dict 오류 처리 추가)&quot;&quot;&quot;
    progress = pyqtSignal(int)
    finished = pyqtSignal(pd.DataFrame)
    error = pyqtSignal(str)
    
    def __init__(self, file_path):
        super().__init__()
        self.file_path = file_path
        
    def safe_json_dumps(self, obj):
        &quot;&quot;&quot;Dict/List 타입을 안전하게 JSON 문자열로 변환&quot;&quot;&quot;
        try:
            if isinstance(obj, (dict, list)):
                return json.dumps(obj, ensure_ascii=False)
            return str(obj)
        except:
            return &quot;[Conversion Error]&quot;

    def convert_complex_types(self, df):
        &quot;&quot;&quot;DataFrame 내의 복합 타입(dict, list)을 문자열로 변환&quot;&quot;&quot;
        for col in df.columns:
            try:
                # 첫 번째 행의 값으로 타입 체크
                sample = df[col].iloc[0] if len(df) &amp;gt; 0 else None
                
                if isinstance(sample, (dict, list)):
                    df[col] = df[col].apply(self.safe_json_dumps)
                elif not pd.api.types.is_string_dtype(df[col]):
                    df[col] = df[col].astype(str)
            except Exception as e:
                print(f&quot;컬럼 {col} 처리 오류: {e}&quot;)
                df[col] = df[col].astype(str)
        return df

    def run(self):
        try:
            self.progress.emit(5)
            
            if self.file_path.endswith('.parquet'):
                # Parquet 파일 로드
                parquet_file = pq.ParquetFile(self.file_path)
                num_row_groups = parquet_file.num_row_groups
                chunks = []
                
                for i in range(num_row_groups):
                    self.progress.emit(10 + int((i+1)/num_row_groups*70))
                    table = parquet_file.read_row_group(i)
                    df = table.to_pandas()
                    df = self.convert_complex_types(df)  # Dict/List 처리
                    chunks.append(df)
                
                self.progress.emit(90)
                result_df = pd.concat(chunks, ignore_index=True)
                
            else:
                # CSV 파일 로드
                chunksize = 100000
                chunks = []
                total_rows = sum(1 for _ in open(self.file_path, 'r', encoding='utf-8')) - 1
                processed_rows = 0
                
                for chunk in pd.read_csv(self.file_path, chunksize=chunksize):
                    progress = 10 + int(processed_rows / total_rows * 70)
                    self.progress.emit(progress)
                    chunk = self.convert_complex_types(chunk)  # Dict/List 처리
                    chunks.append(chunk)
                    processed_rows += len(chunk)
                
                self.progress.emit(90)
                result_df = pd.concat(chunks, ignore_index=True)
            
            self.progress.emit(95)
            result_df = result_df.fillna(&quot;&quot;)  # NULL 값 처리
            self.progress.emit(100)
            self.finished.emit(result_df)
            
        except Exception as e:
            error_msg = f&quot;로딩 실패: {str(e)}\n\n{traceback.format_exc()}&quot;
            self.error.emit(error_msg)

class DataFrameModel(QAbstractTableModel):
    &quot;&quot;&quot;Dict 타입을 안전하게 처리하는 데이터 모델&quot;&quot;&quot;
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        return len(self._data.columns)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        value = self._data.iloc[index.row(), index.column()]

        if role == Qt.DisplayRole:
            return str(value) if not pd.isna(value) else &quot;&quot;
        elif role == Qt.BackgroundRole:
            if isinstance(value, (dict, list)):
                return QColor(240, 248, 255)  # 복합 타입 배경색
            return QColor(255, 255, 255)
        return None

    def headerData(self, section, orientation, role):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self._data.columns[section])
            return str(self._data.index[section])
        return None

class DataViewer(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;Data Viewer with Dict Handling&quot;)
        self.setGeometry(100, 100, 1200, 800)
        self.setup_ui()
        
    def setup_ui(self):
        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)
        layout = QVBoxLayout(self.central_widget)
        
        # 컨트롤 패널
        control_panel = QWidget()
        control_layout = QHBoxLayout(control_panel)
        
        self.btn_open = QPushButton(&quot;파일 열기&quot;)
        self.btn_open.clicked.connect(self.open_file)
        control_layout.addWidget(self.btn_open)
        
        self.search_input = QLineEdit()
        self.search_input.setPlaceholderText(&quot;검색어 입력&quot;)
        control_layout.addWidget(self.search_input)
        
        self.column_combo = QComboBox()
        self.column_combo.addItem(&quot;모든 컬럼&quot;)
        control_layout.addWidget(self.column_combo)
        
        layout.addWidget(control_panel)
        
        # 테이블 뷰
        self.table_view = QTableView()
        self.table_view.setSortingEnabled(True)
        self.proxy_model = QSortFilterProxyModel()
        self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.table_view.setModel(self.proxy_model)
        layout.addWidget(self.table_view)
        
        # 상태바
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
        
        # 로딩 다이얼로그
        self.progress_dialog = QProgressDialog(&quot;파일을 로드 중입니다...&quot;, &quot;취소&quot;, 0, 100, self)
        self.progress_dialog.setWindowModality(Qt.WindowModal)
        self.progress_dialog.canceled.connect(self.cancel_loading)
        
    def open_file(self):
        file_path, _ = QFileDialog.getOpenFileName(
            self, &quot;파일 열기&quot;, &quot;&quot;,
            &quot;데이터 파일 (*.parquet *.csv);;모든 파일 (*)&quot;)
            
        if file_path:
            self.load_data(file_path)
    
    def load_data(self, file_path):
        &quot;&quot;&quot;데이터 로드 및 진행률 표시&quot;&quot;&quot;
        self.progress_dialog.reset()
        self.progress_dialog.show()
        self.btn_open.setEnabled(False)
        
        self.load_worker = LoadWorker(file_path)
        self.load_worker.progress.connect(self.update_progress)
        self.load_worker.finished.connect(self.data_load_complete)
        self.load_worker.error.connect(self.data_load_error)
        self.load_worker.start()
    
    def update_progress(self, value):
        &quot;&quot;&quot;진행률 업데이트&quot;&quot;&quot;
        self.progress_dialog.setValue(value)
        
    def data_load_complete(self, df):
        &quot;&quot;&quot;데이터 로드 완료 처리&quot;&quot;&quot;
        self.progress_dialog.reset()
        self.btn_open.setEnabled(True)
        
        # 데이터 모델 설정
        model = DataFrameModel(df)
        self.proxy_model.setSourceModel(model)
        
        # 컬럼 목록 업데이트
        self.column_combo.clear()
        self.column_combo.addItem(&quot;모든 컬럼&quot;)
        self.column_combo.addItems(df.columns.tolist())
        
        # 상태바 업데이트
        file_size = os.path.getsize(self.load_worker.file_path) / (1024 * 1024)  # MB 단위
        self.status_bar.showMessage(
            f&quot;로드 완료: {len(df):,}행 | {len(df.columns)}열 | {file_size:.2f}MB | &quot;
            f&quot;Dict/List 컬럼: {self.count_complex_columns(df)}개&quot;
        )
    
    def count_complex_columns(self, df):
        &quot;&quot;&quot;Dict/List 타입 컬럼 수 카운트&quot;&quot;&quot;
        count = 0
        for col in df.columns:
            sample = df[col].iloc[0] if len(df) &amp;gt; 0 else None
            if isinstance(sample, (dict, list)):
                count += 1
        return count
    
    def data_load_error(self, error_msg):
        &quot;&quot;&quot;데이터 로드 오류 처리&quot;&quot;&quot;
        self.progress_dialog.reset()
        self.btn_open.setEnabled(True)
        QMessageBox.critical(self, &quot;로드 오류&quot;, error_msg)
        self.status_bar.showMessage(&quot;로드 실패&quot;)
    
    def cancel_loading(self):
        &quot;&quot;&quot;로딩 취소&quot;&quot;&quot;
        if hasattr(self, 'load_worker') and self.load_worker.isRunning():
            self.load_worker.terminate()
        self.progress_dialog.reset()
        self.btn_open.setEnabled(True)
        self.status_bar.showMessage(&quot;로딩 취소됨&quot;)

if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    app.setStyle('Fusion')
    viewer = DataViewer()
    viewer.show()
    sys.exit(app.exec_())&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1052</guid>
      <comments>https://ljj777.tistory.com/1052#entry1052comment</comments>
      <pubDate>Wed, 30 Apr 2025 13:08:06 +0900</pubDate>
    </item>
    <item>
      <title>Parquet 기능</title>
      <link>https://ljj777.tistory.com/1051</link>
      <description>&lt;pre id=&quot;code_1745973661293&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
import os
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import json
import traceback
from concurrent.futures import ThreadPoolExecutor
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QTableView, QFileDialog,
    QVBoxLayout, QWidget, QPushButton, QLabel,
    QStatusBar, QMessageBox, QLineEdit, QHBoxLayout,
    QComboBox, QHeaderView, QProgressDialog, QCheckBox
)
from PyQt5.QtCore import (
    Qt, QAbstractTableModel, QSortFilterProxyModel, 
    QThread, pyqtSignal, QObject, QRunnable, QThreadPool
)
from PyQt5.QtGui import QColor, QFont

class WorkerSignals(QObject):
    &quot;&quot;&quot;작업자 스레드 시그널&quot;&quot;&quot;
    progress = pyqtSignal(int)
    finished = pyqtSignal(object)
    error = pyqtSignal(str)
    message = pyqtSignal(str)

class ExportWorker(QRunnable):
    &quot;&quot;&quot;내보내기 작업을 처리하는 Runnable&quot;&quot;&quot;
    def __init__(self, df, file_path, file_type):
        super().__init__()
        self.df = df
        self.file_path = file_path
        self.file_type = file_type
        self.signals = WorkerSignals()
        self._is_running = True

    def run(self):
        try:
            if not self._is_running:
                return

            self.signals.message.emit(f&quot;Exporting to {self.file_type}...&quot;)
            
            if self.file_type == &quot;CSV (*.csv)&quot;:
                self.export_csv()
            elif self.file_type == &quot;Excel (*.xlsx)&quot;:
                self.export_excel()
            elif self.file_type == &quot;Parquet (*.parquet)&quot;:
                self.export_parquet()
            elif self.file_type == &quot;JSON (*.json)&quot;:
                self.export_json()
            
            if self._is_running:
                self.signals.finished.emit(self.file_path)
                self.signals.message.emit(&quot;Export completed successfully&quot;)

        except Exception as e:
            self.signals.error.emit(f&quot;Export failed: {str(e)}&quot;)
            self.signals.message.emit(&quot;Export failed&quot;)

    def export_csv(self):
        &quot;&quot;&quot;CSV 형식으로 내보내기&quot;&quot;&quot;
        chunksize = 100000
        total_rows = len(self.df)
        
        for i in range(0, total_rows, chunksize):
            if not self._is_running:
                return
                
            chunk = self.df.iloc[i:i+chunksize]
            mode = 'w' if i == 0 else 'a'
            header = (i == 0)
            
            chunk.to_csv(
                self.file_path,
                mode=mode,
                header=header,
                index=False
            )
            
            progress = int((i + chunksize) / total_rows * 100)
            self.signals.progress.emit(min(progress, 100))

    def export_excel(self):
        &quot;&quot;&quot;Excel 형식으로 내보내기&quot;&quot;&quot;
        if not self._is_running:
            return
            
        self.signals.progress.emit(20)
        try:
            self.df.to_excel(self.file_path, index=False, engine='openpyxl')
        except ImportError:
            self.df.to_excel(self.file_path, index=False)
        self.signals.progress.emit(100)

    def export_parquet(self):
        &quot;&quot;&quot;Parquet 형식으로 내보내기&quot;&quot;&quot;
        if not self._is_running:
            return
            
        self.signals.progress.emit(30)
        self.df.to_parquet(self.file_path, engine='pyarrow')
        self.signals.progress.emit(100)

    def export_json(self):
        &quot;&quot;&quot;JSON 형식으로 내보내기&quot;&quot;&quot;
        if not self._is_running:
            return
            
        self.signals.progress.emit(10)
        
        chunksize = 50000
        total_rows = len(self.df)
        
        with open(self.file_path, 'w', encoding='utf-8') as f:
            for i in range(0, total_rows, chunksize):
                if not self._is_running:
                    return
                    
                chunk = self.df.iloc[i:i+chunksize]
                json_str = chunk.to_json(orient='records', lines=True, force_ascii=False)
                f.write(json_str)
                
                progress = int((i + chunksize) / total_rows * 100)
                self.signals.progress.emit(min(progress, 100))

    def stop(self):
        &quot;&quot;&quot;작업 중단&quot;&quot;&quot;
        self._is_running = False

class ArrowTableConverter:
    &quot;&quot;&quot;모든 PyArrow 버전에서 안전하게 테이블 변환&quot;&quot;&quot;
    @staticmethod
    def get_columns(table):
        &quot;&quot;&quot;모든 버전에서 컬럼 이름 추출&quot;&quot;&quot;
        if hasattr(table, 'column_names'):
            return table.column_names
        return [table.schema[i].name for i in range(table.num_columns)]

    @staticmethod
    def get_column_data(table, col):
        &quot;&quot;&quot;모든 버전에서 컬럼 데이터 추출&quot;&quot;&quot;
        if hasattr(table, 'column'):
            return table.column(col)
        return table[col]

    @staticmethod
    def to_dataframe(table):
        &quot;&quot;&quot;테이블을 DataFrame으로 안전하게 변환&quot;&quot;&quot;
        try:
            return table.to_pandas()
        except:
            return ArrowTableConverter.manual_conversion(table)

    @staticmethod
    def manual_conversion(table):
        &quot;&quot;&quot;수동 테이블 변환 (최후의 방법)&quot;&quot;&quot;
        data = {}
        columns = ArrowTableConverter.get_columns(table)
        
        for col in columns:
            try:
                col_data = ArrowTableConverter.get_column_data(table, col)
                if hasattr(col_data, 'to_pandas'):
                    data[col] = col_data.to_pandas()
                else:
                    data[col] = [str(x) for x in col_data]
            except:
                data[col] = [&quot;[Conversion Error]&quot;] * len(table)
        
        return pd.DataFrame(data)

class ParquetLoader(QThread):
    &quot;&quot;&quot;강력한 Parquet 파일 로더&quot;&quot;&quot;
    def __init__(self, file_path, max_rows=None):
        super().__init__()
        self.file_path = file_path
        self.max_rows = max_rows
        self.signals = WorkerSignals()
        self._is_running = True

    def run(self):
        try:
            self.signals.progress.emit(5)
            parquet_file = pq.ParquetFile(self.file_path)
            num_row_groups = parquet_file.num_row_groups
            self.signals.progress.emit(10)

            chunks = []
            loaded_rows = 0
            
            for i in range(num_row_groups):
                if not self._is_running:
                    return
                
                self.signals.progress.emit(10 + int((i+1)/num_row_groups*70))
                table = parquet_file.read_row_group(i)
                df_chunk = ArrowTableConverter.to_dataframe(table)
                
                if self.max_rows:
                    remaining = self.max_rows - loaded_rows
                    if remaining &amp;lt;= 0:
                        break
                    df_chunk = df_chunk.head(remaining)
                
                chunks.append(df_chunk)
                loaded_rows += len(df_chunk)
                
                if self.max_rows and loaded_rows &amp;gt;= self.max_rows:
                    break

            self.signals.progress.emit(85)
            combined_df = pd.concat(chunks, ignore_index=True)
            final_df = self.clean_data(combined_df)
            
            self.signals.progress.emit(100)
            self.signals.finished.emit(final_df)
            
        except Exception as e:
            error_trace = traceback.format_exc()
            self.signals.error.emit(f&quot;로딩 실패:\n{str(e)}\n\n{error_trace}&quot;)
        finally:
            if 'parquet_file' in locals():
                del parquet_file

    def clean_data(self, df):
        &quot;&quot;&quot;데이터 정제&quot;&quot;&quot;
        for col in df.columns:
            try:
                df[col] = df[col].fillna(&quot;&quot;)
                sample = df[col].iloc[0] if len(df) &amp;gt; 0 else None
                if isinstance(sample, (dict, list)):
                    df[col] = df[col].apply(self.safe_json_dumps)
                elif not pd.api.types.is_string_dtype(df[col]):
                    df[col] = df[col].astype(str)
            except Exception as col_error:
                print(f&quot;컬럼 {col} 정제 오류: {col_error}&quot;)
                df[col] = &quot;[Error] &quot; + df[col].astype(str)
        return df

    def safe_json_dumps(self, value):
        &quot;&quot;&quot;안전한 JSON 변환&quot;&quot;&quot;
        try:
            if value is None:
                return &quot;&quot;
            if isinstance(value, (dict, list)):
                return json.dumps(value, ensure_ascii=False, default=str)[:2000]
            return str(value)
        except:
            return &quot;[Conversion Error]&quot;

    def stop(self):
        &quot;&quot;&quot;작업 중단&quot;&quot;&quot;
        self._is_running = False

class DataFrameModel(QAbstractTableModel):
    &quot;&quot;&quot;고성능 데이터 모델&quot;&quot;&quot;
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, parent=None):
        return len(self._data)

    def columnCount(self, parent=None):
        return len(self._data.columns)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        value = self._data.iloc[index.row(), index.column()]

        if role == Qt.DisplayRole:
            return str(value) if not pd.isna(value) else &quot;&quot;
        elif role == Qt.BackgroundRole:
            if isinstance(value, (dict, list)):
                return QColor(240, 248, 255)
            return QColor(255, 255, 255)
        elif role == Qt.TextAlignmentRole:
            return Qt.AlignLeft | Qt.AlignVCenter
        return None

    def headerData(self, section, orientation, role):
        if role != Qt.DisplayRole:
            return None
        if orientation == Qt.Horizontal:
            return str(self._data.columns[section])
        return str(self._data.index[section])

class ParquetViewer(QMainWindow):
    &quot;&quot;&quot;메인 뷰어 클래스&quot;&quot;&quot;
    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;Universal Parquet Viewer&quot;)
        self.setGeometry(100, 100, 1400, 900)
        self.setup_ui()
        self.thread_pool = QThreadPool.globalInstance()
        self.thread_pool.setMaxThreadCount(2)
        self.export_worker = None
        
    def setup_ui(self):
        &quot;&quot;&quot;UI 초기화&quot;&quot;&quot;
        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)
        layout = QVBoxLayout(self.central_widget)
        
        # 컨트롤 패널
        panel = QWidget()
        panel_layout = QHBoxLayout(panel)
        
        self.btn_open = QPushButton(&quot;Open Parquet&quot;)
        self.btn_open.clicked.connect(self.open_file)
        panel_layout.addWidget(self.btn_open)
        
        self.preview_check = QCheckBox(&quot;Preview Mode (First 1,000 rows)&quot;)
        self.preview_check.setChecked(True)
        panel_layout.addWidget(self.preview_check)
        
        panel_layout.addWidget(QLabel(&quot;Search:&quot;))
        self.search_input = QLineEdit()
        self.search_input.setPlaceholderText(&quot;Search...&quot;)
        self.search_input.textChanged.connect(self.apply_filter)
        self.search_input.setEnabled(False)
        panel_layout.addWidget(self.search_input)
        
        self.column_combo = QComboBox()
        self.column_combo.addItem(&quot;All Columns&quot;)
        self.column_combo.setEnabled(False)
        panel_layout.addWidget(self.column_combo)
        
        self.btn_export = QPushButton(&quot;Export&quot;)
        self.btn_export.clicked.connect(self.export_data)
        self.btn_export.setEnabled(False)
        panel_layout.addWidget(self.btn_export)
        
        layout.addWidget(panel)
        
        # 테이블 뷰
        self.table_view = QTableView()
        self.table_view.setSortingEnabled(True)
        self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
        self.table_view.setStyleSheet(&quot;&quot;&quot;
            QTableView {
                font-size: 10pt;
                selection-background-color: #3498db;
                selection-color: white;
            }
            QHeaderView::section {
                background-color: #34495e;
                color: white;
                padding: 5px;
                font-weight: bold;
            }
        &quot;&quot;&quot;)
        layout.addWidget(self.table_view)
        
        # 상태 바
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
        
        # 프록시 모델
        self.proxy_model = QSortFilterProxyModel()
        self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
    
    def open_file(self):
        &quot;&quot;&quot;파일 열기 다이얼로그&quot;&quot;&quot;
        options = QFileDialog.Options()
        file_path, _ = QFileDialog.getOpenFileName(
            self, &quot;Open Parquet File&quot;, &quot;&quot;, 
            &quot;Parquet Files (*.parquet);;All Files (*)&quot;, 
            options=options)
        
        if file_path:
            self.load_parquet(file_path)
    
    def load_parquet(self, file_path):
        &quot;&quot;&quot;Parquet 파일 로드&quot;&quot;&quot;
        self.progress = QProgressDialog(&quot;Loading...&quot;, &quot;Cancel&quot;, 0, 100, self)
        self.progress.setWindowModality(Qt.WindowModal)
        self.progress.canceled.connect(self.cancel_loading)
        
        max_rows = 1000 if self.preview_check.isChecked() else None
        self.loader = ParquetLoader(file_path, max_rows=max_rows)
        self.loader.signals.progress.connect(self.update_progress)
        self.loader.signals.finished.connect(self.on_load_complete)
        self.loader.signals.error.connect(self.on_load_error)
        self.loader.start()
        
        self.progress.show()
    
    def update_progress(self, value):
        &quot;&quot;&quot;진행률 업데이트&quot;&quot;&quot;
        self.progress.setValue(value)
    
    def cancel_loading(self):
        &quot;&quot;&quot;로딩 취소&quot;&quot;&quot;
        if hasattr(self, 'loader'):
            self.loader.stop()
        self.progress.close()
        self.status_bar.showMessage(&quot;Loading canceled&quot;, 3000)
    
    def on_load_complete(self, df):
        &quot;&quot;&quot;로딩 완료 처리&quot;&quot;&quot;
        self.progress.close()
        
        model = DataFrameModel(df)
        self.proxy_model.setSourceModel(model)
        self.table_view.setModel(self.proxy_model)
        self.table_view.resizeColumnsToContents()
        
        self.column_combo.clear()
        self.column_combo.addItem(&quot;All Columns&quot;)
        self.column_combo.addItems(df.columns.tolist())
        self.column_combo.setEnabled(True)
        
        self.search_input.setEnabled(True)
        self.btn_export.setEnabled(True)
        
        file_size = os.path.getsize(self.loader.file_path) / (1024 * 1024)
        self.status_bar.showMessage(
            f&quot;Loaded: {len(df):,} rows | {len(df.columns)} cols | {file_size:.2f} MB&quot;, 
            5000
        )
    
    def on_load_error(self, error_msg):
        &quot;&quot;&quot;로딩 오류 처리&quot;&quot;&quot;
        self.progress.close()
        QMessageBox.critical(self, &quot;Load Error&quot;, error_msg)
        self.status_bar.showMessage(&quot;Load failed&quot;, 5000)
    
    def apply_filter(self, text):
        &quot;&quot;&quot;테이블 필터링 적용&quot;&quot;&quot;
        if not hasattr(self, 'proxy_model') or not hasattr(self.proxy_model, 'sourceModel'):
            return
            
        if self.column_combo.currentText() == &quot;All Columns&quot;:
            self.proxy_model.setFilterKeyColumn(-1)
        else:
            try:
                col_idx = self.proxy_model.sourceModel()._data.columns.get_loc(
                    self.column_combo.currentText())
                self.proxy_model.setFilterKeyColumn(col_idx)
            except (AttributeError, KeyError):
                return
                
        self.proxy_model.setFilterFixedString(text)
    
    def export_data(self):
        &quot;&quot;&quot;데이터 내보내기&quot;&quot;&quot;
        if not hasattr(self, 'proxy_model') or not hasattr(self.proxy_model, 'sourceModel'):
            QMessageBox.warning(
                self, 
                &quot;No Data&quot;, 
                &quot;Please load a Parquet file first before exporting.&quot;,
                QMessageBox.Ok
            )
            self.status_bar.showMessage(&quot;Export failed: No data loaded&quot;, 3000)
            return
            
        df = self.proxy_model.sourceModel()._data
        
        options = QFileDialog.Options()
        file_path, selected_filter = QFileDialog.getSaveFileName(
            self, &quot;Export Data&quot;, &quot;&quot;, 
            &quot;CSV (*.csv);;Excel (*.xlsx);;Parquet (*.parquet);;JSON (*.json)&quot;, 
            options=options)
        
        if not file_path:
            return
            
        if selected_filter == &quot;CSV (*.csv)&quot; and not file_path.endswith('.csv'):
            file_path += '.csv'
        elif selected_filter == &quot;Excel (*.xlsx)&quot; and not file_path.endswith('.xlsx'):
            file_path += '.xlsx'
        elif selected_filter == &quot;Parquet (*.parquet)&quot; and not file_path.endswith('.parquet'):
            file_path += '.parquet'
        elif selected_filter == &quot;JSON (*.json)&quot; and not file_path.endswith('.json'):
            file_path += '.json'
        
        self.export_progress = QProgressDialog(&quot;Exporting...&quot;, &quot;Cancel&quot;, 0, 100, self)
        self.export_progress.setWindowModality(Qt.WindowModal)
        self.export_progress.canceled.connect(self.cancel_export)
        
        self.export_worker = ExportWorker(df, file_path, selected_filter)
        self.export_worker.signals.progress.connect(self.export_progress.setValue)
        self.export_worker.signals.message.connect(self.status_bar.showMessage)
        self.export_worker.signals.finished.connect(self.on_export_complete)
        self.export_worker.signals.error.connect(self.on_export_error)
        
        self.thread_pool.start(self.export_worker)
        self.export_progress.show()
    
    def cancel_export(self):
        &quot;&quot;&quot;내보내기 취소&quot;&quot;&quot;
        if self.export_worker:
            self.export_worker.stop()
        self.export_progress.close()
        self.status_bar.showMessage(&quot;Export canceled&quot;, 3000)
    
    def on_export_complete(self, file_path):
        &quot;&quot;&quot;내보내기 완료 처리&quot;&quot;&quot;
        self.export_progress.close()
        QMessageBox.information(
            self, 
            &quot;Success&quot;, 
            f&quot;Data exported to:\n{file_path}&quot;
        )
        self.status_bar.showMessage(f&quot;Exported to {file_path}&quot;, 5000)
    
    def on_export_error(self, error_msg):
        &quot;&quot;&quot;내보내기 오류 처리&quot;&quot;&quot;
        self.export_progress.close()
        QMessageBox.critical(
            self, 
            &quot;Export Error&quot;, 
            f&quot;Export failed:\n{error_msg}&quot;
        )
        self.status_bar.showMessage(&quot;Export failed&quot;, 5000)

if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    app.setStyle('Fusion')
    
    if hasattr(Qt, 'AA_EnableHighDpiScaling'):
        app.setAttribute(Qt.AA_EnableHighDpiScaling, True)
    if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
        app.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
    
    viewer = ParquetViewer()
    viewer.show()
    sys.exit(app.exec_())&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1051</guid>
      <comments>https://ljj777.tistory.com/1051#entry1051comment</comments>
      <pubDate>Wed, 30 Apr 2025 08:08:25 +0900</pubDate>
    </item>
    <item>
      <title>parquet 뷰어</title>
      <link>https://ljj777.tistory.com/1050</link>
      <description>&lt;pre id=&quot;code_1745906260187&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip install pyarrow pandas PyQt5==5.15.4 PyQt5-sip==12.8.1 PyQt5==5.15.2&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1745911113118&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pyinstaller --onefile --hidden-import=fastparquet --noconsole parquet_redy.py&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1745909349822&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;a = Analysis(
    ['parquet_viewer.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[
        'fastparquet',
        'fastparquet.speedups',  # fastparquet의 C 확장 모듈
        'pandas',
        'pyarrow'
    ],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1745909659069&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
import pandas as pd
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QTableView, QFileDialog,
    QVBoxLayout, QWidget, QPushButton, QLabel,
    QStatusBar, QMessageBox
)
from PyQt5.QtCore import Qt, QAbstractTableModel

class PandasModel(QAbstractTableModel):
    &quot;&quot;&quot;Pandas DataFrame을 QTableView에 표시하기 위한 모델&quot;&quot;&quot;
    def __init__(self, data):
        QAbstractTableModel.__init__(self)
        self._data = data

    def rowCount(self, parent=None):
        return self._data.shape[0]

    def columnCount(self, parent=None):
        return self._data.shape[1]

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole:
                return str(self._data.iloc[index.row(), index.column()])
        return None

    def headerData(self, section, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self._data.columns[section]
        if orientation == Qt.Vertical and role == Qt.DisplayRole:
            return str(self._data.index[section])
        return None

class ParquetViewer(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;Parquet File Viewer (PyArrow Only)&quot;)
        self.setGeometry(100, 100, 1000, 800)
        
        # 메인 위젯과 레이아웃 설정
        self.main_widget = QWidget()
        self.setCentralWidget(self.main_widget)
        self.layout = QVBoxLayout(self.main_widget)
        
        # 파일 열기 버튼
        self.open_button = QPushButton(&quot;Open Parquet File&quot;)
        self.open_button.clicked.connect(self.open_file)
        self.layout.addWidget(self.open_button)
        
        # 파일 정보 표시 레이블
        self.file_info_label = QLabel(&quot;No file loaded&quot;)
        self.file_info_label.setStyleSheet(&quot;font-weight: bold; color: #333;&quot;)
        self.layout.addWidget(self.file_info_label)
        
        # 테이블 뷰
        self.table_view = QTableView()
        self.table_view.setStyleSheet(&quot;QTableView { font-size: 10pt; }&quot;)
        self.layout.addWidget(self.table_view)
        
        # 상태 표시줄
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
        
        # 초기 데이터
        self.df = pd.DataFrame()
        
    def open_file(self):
        &quot;&quot;&quot;파일 다이얼로그를 열고 선택한 Parquet 파일을 로드&quot;&quot;&quot;
        options = QFileDialog.Options()
        file_name, _ = QFileDialog.getOpenFileName(
            self, &quot;Open Parquet File&quot;, &quot;&quot;, 
            &quot;Parquet Files (*.parquet);;All Files (*)&quot;, 
            options=options)
        
        if file_name:
            try:
                # PyArrow 엔진으로 명시적 지정
                self.df = pd.read_parquet(file_name, engine='pyarrow')
                
                # 딕셔너리/리스트 타입 컬럼 처리
                for col in self.df.columns:
                    if self.df[col].apply(lambda x: isinstance(x, (dict, list))).any():
                        self.df[col] = self.df[col].astype(str)
                
                # 모델 설정
                model = PandasModel(self.df)
                self.table_view.setModel(model)
                self.table_view.resizeColumnsToContents()
                
                # 파일 정보 업데이트
                self.file_info_label.setText(
                    f&quot;File: {file_name.split('/')[-1]} | &quot;
                    f&quot;Rows: {len(self.df):,} | &quot;
                    f&quot;Columns: {len(self.df.columns)} | &quot;
                    f&quot;Engine: PyArrow&quot;)
                
                self.status_bar.showMessage(&quot;File loaded successfully&quot;, 3000)
                
            except Exception as e:
                error_msg = f&quot;Error: {str(e)}\n\nRequired: pip install pyarrow&quot;
                QMessageBox.critical(self, &quot;Load Error&quot;, error_msg)
                self.status_bar.showMessage(&quot;Error: Install pyarrow first&quot;, 5000)

if __name__ == &quot;__main__&quot;:
    app = QApplication(sys.argv)
    app.setStyle('Fusion')
    
    # 폰트 설정
    font = app.font()
    font.setPointSize(10)
    app.setFont(font)
    
    viewer = ParquetViewer()
    viewer.show()
    sys.exit(app.exec_())&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1050</guid>
      <comments>https://ljj777.tistory.com/1050#entry1050comment</comments>
      <pubDate>Tue, 29 Apr 2025 14:58:39 +0900</pubDate>
    </item>
    <item>
      <title>s3디버깅</title>
      <link>https://ljj777.tistory.com/1049</link>
      <description>&lt;pre id=&quot;code_1745473733565&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try:
    s3.put_object(...)
except Exception as e:
    print(f&quot;Error: {e.response['Error']['Code']}&quot;)  # AccessDenied, KMS.Disabled 등&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1049</guid>
      <comments>https://ljj777.tistory.com/1049#entry1049comment</comments>
      <pubDate>Thu, 24 Apr 2025 14:48:59 +0900</pubDate>
    </item>
    <item>
      <title>parquet</title>
      <link>https://ljj777.tistory.com/1048</link>
      <description>&lt;pre id=&quot;code_1745390802048&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import pandas as pd

# Parquet 파일 읽기
df = pd.read_parquet('example.parquet')

# 전체 데이터 출력
print(&quot;전체 데이터:&quot;)
print(df)

# 상위 5행 출력
print(&quot;\n상위 5행:&quot;)
print(df.head())

# 데이터 구조 확인
print(&quot;\n데이터 구조:&quot;)
print(df.info())

# 기술 통계 정보
print(&quot;\n기술 통계:&quot;)
print(df.describe())&lt;/code&gt;&lt;/pre&gt;</description>
      <category>랭귀지/pandas</category>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1048</guid>
      <comments>https://ljj777.tistory.com/1048#entry1048comment</comments>
      <pubDate>Wed, 23 Apr 2025 15:47:08 +0900</pubDate>
    </item>
    <item>
      <title>Yml</title>
      <link>https://ljj777.tistory.com/1047</link>
      <description>&lt;pre id=&quot;code_1745371255513&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import yaml

# YAML 파일 로드
with open('config.yml') as f:
    config = yaml.safe_load(f)

# 값 접근
print(config['app']['name'])  # &quot;My Awesome App&quot;
print(config['database']['production']['credentials']['username'])  # &quot;admin&quot;

# 리스트 항목 접근
for feature in config['app']['features']:
    print(feature)&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1745371237778&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app:
  name: &quot;My Awesome App&quot;
  version: 2.3.1
  features:
    - &quot;authentication&quot;
    - &quot;data_export&quot;
    - &quot;notifications&quot;
  settings:
    cache_enabled: true
    max_retries: 3
    allowed_file_types: [&quot;.jpg&quot;, &quot;.png&quot;, &quot;.pdf&quot;]&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1047</guid>
      <comments>https://ljj777.tistory.com/1047#entry1047comment</comments>
      <pubDate>Wed, 23 Apr 2025 10:21:05 +0900</pubDate>
    </item>
    <item>
      <title>.env yaml</title>
      <link>https://ljj777.tistory.com/1045</link>
      <description>&lt;pre id=&quot;code_1745365286795&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DB_HOST: localhost
DB_PORT: 3306
DB_USER: root
DB_PASSWORD: secret&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745365318189&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import yaml

with open(&quot;env.yml&quot;, &quot;r&quot;) as f:
    config = yaml.safe_load(f)

print(config[&quot;DB_HOST&quot;])&lt;/code&gt;&lt;/pre&gt;</description>
      <author>유키공</author>
      <guid isPermaLink="true">https://ljj777.tistory.com/1045</guid>
      <comments>https://ljj777.tistory.com/1045#entry1045comment</comments>
      <pubDate>Wed, 23 Apr 2025 08:42:29 +0900</pubDate>
    </item>
  </channel>
</rss>